Compare commits

...

663 Commits

Author SHA1 Message Date
codl 50ba3dfaa0
Delete dependabot.yml 2022-09-16 21:48:05 +02:00
codl 222dca4fb1
Update README.markdown 2022-09-15 13:55:17 +02:00
codl 1524554a40 docker: cache pip http cache only, not wheel cache 2022-08-20 00:08:43 +02:00
codl 70b90366b6 dockerfile: use cache mounts 2022-08-11 19:06:56 +02:00
codl 9ac6227ca5 release v2.2.0 2022-08-10 09:39:08 +02:00
codl 5afa982cf3 docker-compose: use image from ghcr 2022-08-10 09:37:13 +02:00
codl 0fdff9ff6e docker publish: create semver tags 2022-08-10 09:36:28 +02:00
codl 43ba8e1362
Create docker-publish.yml 2022-08-10 09:22:29 +02:00
codl 3c372a2afb docker: add .git
it's dirty but it's needed for versioneer

I will figure out a better way to do this later
2022-08-05 02:23:54 +02:00
codl c82c15e0da further dockerfile caching and size optim
nodejs and js libraries are only needed at build time
2022-08-05 02:23:54 +02:00
codl 3497a63cff update changelog 2022-08-05 02:23:54 +02:00
codl 30b7b24e68 lots of docker nitpicks
- changed all service names to not include "forget", let docker-compose
  generate container names

- gave the image a single name

- listen on 127.0.0.1 by default, provide documented override file that
  reverses it

- docs: on that note: don't tell folks to edit docker-compose.yml directly

- docs: explicitly say "run docker-compose build" because docker-compose
  is janky and it can be surprising that downloading a new version of
  the code and running `docker-compose up` doesn't do anything
2022-08-05 02:23:54 +02:00
codl fddcbbe8a0 reorganize dockerfile for better build cache usage
also lock some versions

ideally I wouldn't have to copy each of these directories separately and
have a janky stage to hide all the resulting layers, but docker do be like that.
`COPY dir1 dir2 ./` copies *the contents of* dir1 and dir2 to the
working directory :(

maybe one day i will switch to having a src directory
2022-08-05 01:39:36 +02:00
shibao 59095ae1ef get rid of celery logfiles 2022-07-30 19:21:52 -04:00
shibao de2329d041 remove queue from worker 2022-07-30 19:13:59 -04:00
shibao 69cb8db391 build locally 2022-07-30 18:32:15 -04:00
shibao 4f2770e4e2 remove unnecessary pid file location 2022-07-30 16:03:49 -04:00
shibao d2bb9094f0 fix celery beat 2022-07-30 15:40:41 -04:00
shibao b89122edb5 grammar 2022-07-30 12:53:17 -04:00
shibao 5872ce6da8 use image in docker-compose.yml 2022-07-30 03:39:47 -04:00
shibao 659bb1dfb8 add docker stuff 2022-07-30 03:39:39 -04:00
codl f195ed3261
codacy is no longer allowed in my house 2022-03-04 23:39:54 +01:00
codl 5fdb99256f limit workflow to pushes and prs to master 2022-03-04 20:38:23 +01:00
codl 916a47ef9d update versioneer 2022-03-04 20:22:55 +01:00
codl 9c035d5132 actions: cache deps 2022-03-04 20:22:55 +01:00
codl 38a1c543af tests & github actions: replace pytest-redis with local redis 2022-03-04 20:22:55 +01:00
codl 1354d415d3 add github actions workflow, remove travis.yml 2022-03-04 20:22:55 +01:00
codl 44632934a7 Merge branch 'feature/instance-hidelist' 2022-03-04 14:20:00 +01:00
codl 299a99844f update changelog 2022-03-04 14:05:03 +01:00
codl c621982424 remove method to upgrade known instances from cookie to localstorage
it's been over three years since the old forget_known_instances cookie
is no longer being set, any cookie that hasn't been upgraded to
localstorage by now has surely expired
2022-03-04 14:03:08 +01:00
codl 82a72bfd32 fix 500s on login pages 2022-03-04 13:28:38 +01:00
codl 917202de1d update changelog 2022-03-04 13:20:20 +01:00
codl bbbb2470ed add instance hidelist 2022-03-04 13:12:35 +01:00
codl 77bb52cb9e release v2.1.0 2022-03-04 12:34:17 +01:00
codl 688b787c16 fix logged in template 2022-03-02 20:47:47 +01:00
Johann150 88760deb6f
add permission to read reactions 2022-03-02 13:30:40 +01:00
Johann150 aacc5100de
clarify what favourite means on misskey
This implementation does not actually read favourites, but reactions,
because reading favourites is more involved as they are like bookmarks
on Mastodon and not attached to the post itself.

Also removing the respective erroneous authentication scope.
2022-03-01 15:44:16 +01:00
Johann150 4efe6ec316
also display direct message settings for misskey 2022-03-01 15:38:23 +01:00
codl d5e0a364a8 mellow dependabot out (daily → monthly) 2022-03-01 01:13:25 +01:00
codl ce1990cd0d fix promo image in readme 2022-03-01 01:12:06 +01:00
codl 61700f3dd9 fix deps 2022-03-01 00:59:55 +01:00
codl a66ad7db9c deps update 2022-03-01 00:16:24 +01:00
codl 1eacfca8b4 upgrade package-lock to v2 2022-03-01 00:10:50 +01:00
codl a6a4416254 copyedit README, /about/ 2022-03-01 00:09:04 +01:00
codl c7762e839b update changelog 2022-02-28 22:45:25 +01:00
codl 6a14b70d88 fix migration 2022-02-28 22:39:10 +01:00
codl 1c65cd2556 also update date when updating batch end 2022-02-27 13:20:24 +01:00
codl a85095cd00 remove fk from fetch_current_batch_end_id
an attempt at fixing #584
2022-02-27 13:12:47 +01:00
codl 825313091c Merge branch 'cleanup-readme' 2022-02-27 00:43:08 +01:00
codl 9d2147e905 Merge branch 'misskey' 2022-02-27 00:38:03 +01:00
codl 20c0c93a5e update README, CHANGELOG 2022-02-27 00:34:29 +01:00
codl 1cade39fb9 misskey: fetch 100 notes at a time 2022-02-27 00:25:25 +01:00
codl 21eff570a0 fix default misskey known instances 2022-02-26 23:14:40 +01:00
codl 00bf83388f
bump maintenance year, remove messed up codacy badge 2022-02-26 22:10:29 +01:00
Johann150 b54a26cf8f
fix exception when no posts are in database
Instead of using Query.one use Query.one_or_none. The check after
setting the variable suggests this was the intended behaviour instead
of throwing errors if there are no posts in the database.
2021-12-09 18:57:00 +01:00
codl f7f2276cec
known_instances+misskey: actually record mk instances when logged into 2021-12-09 17:45:45 +01:00
codl e5971e3848
add misskey support to known instances 2021-12-09 16:31:48 +01:00
Johann150 e7744a1964
remove Miauth authentication
When using the "log out" functionality of Miauth and logging back
in again on forget, a new API token will be generated instead of
using the old one.
2021-11-16 19:03:01 +01:00
Johann150 77a2687d9e
fix rendering of misskey instances in template 2021-11-11 10:44:06 +01:00
Johann150 37652e4053
add drop type to fully reverse migration 2021-11-11 10:43:15 +01:00
Johann150 ab2c5c9aae
adapt privacy policy to misskey 2021-11-11 09:09:16 +01:00
Johann150 1522d766fa
correctly read miauth feature flag 2021-11-10 12:53:34 +01:00
Johann150 623a7e4415
fix all kinds of mistakes 2021-11-10 12:33:55 +01:00
Johann150 fce0f88d2c
fix variable name 2021-11-10 07:56:44 +01:00
Johann150 dbd7193636
use requests session 2021-11-10 07:56:40 +01:00
Johann150 ba72b8acf9
add misskey migration 2021-11-10 00:49:15 +01:00
Johann150 bb496bb5e6
fix parsing urls 2021-11-09 23:43:09 +01:00
Johann150 eee3bb82fe
fix small miscellaneous bugs 2021-11-09 23:07:46 +01:00
Johann150 8214cda672
fix IDs for instance JSON data 2021-11-09 23:06:44 +01:00
Johann150 8b5f56bef2
use urllib to parse instance url 2021-11-09 23:05:57 +01:00
Johann150 b20395cc8f
add misskey-colored css 2021-11-09 10:08:30 +01:00
Johann150 ed1c42d30d
WIP: implement misskey API 2021-11-09 10:07:56 +01:00
Johann150 ce35aa939b
add misskey to web pages 2021-11-09 09:23:22 +01:00
Johann150 05db96236c
add misskey icon 2021-11-09 09:21:32 +01:00
codl 159c1826d3 removing counters: take two 2021-05-14 20:32:28 +02:00
codl f3e68e7fb4
Merge pull request #460 from codl/better-words
better terminology
2021-05-14 20:29:12 +02:00
codl 3c0b017141
Merge pull request #461 from codl/remove-counters
remove fav/reblog counters. they were never used
2021-05-14 20:03:35 +02:00
codl 8c750d3207 remove fav/reblog counters. they were never used 2021-05-14 19:46:59 +02:00
codl 85b716c11c
header_whitelist → allowed_headers 2021-05-14 18:47:46 +02:00
codl 98bee9b1cd
blacklist → blocklist 2021-05-14 18:46:42 +02:00
dependabot[bot] 96f4c74d8f
Merge pull request #429 from codl/dependabot/pip/alembic-1.5.8 2021-03-25 12:20:31 +00:00
dependabot[bot] 1f15a87d97 Bump rollup from 2.41.5 to 2.42.4
Bumps [rollup](https://github.com/rollup/rollup) from 2.41.5 to 2.42.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.41.5...v2.42.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-25 13:19:48 +01:00
dependabot[bot] f2a2976c08
Bump alembic from 1.5.7 to 1.5.8
Bumps [alembic](https://alembic.sqlalchemy.org) from 1.5.7 to 1.5.8.

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-24 05:25:40 +00:00
dependabot[bot] 25aa3c3844
Merge pull request #423 from codl/dependabot/npm_and_yarn/rollup-2.41.5 2021-03-20 00:50:08 +00:00
dependabot[bot] 1dec832666
Bump rollup from 2.41.4 to 2.41.5
Bumps [rollup](https://github.com/rollup/rollup) from 2.41.4 to 2.41.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.41.4...v2.41.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-18 05:23:36 +00:00
dependabot[bot] 0ac2c95284
Merge pull request #422 from codl/dependabot/npm_and_yarn/rollup-2.41.4 2021-03-17 22:21:02 +00:00
dependabot[bot] 6092398dcc
Bump rollup from 2.41.1 to 2.41.4
Bumps [rollup](https://github.com/rollup/rollup) from 2.41.1 to 2.41.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.41.1...v2.41.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-17 05:22:04 +00:00
codl 521cd7b1dc status check: check redis (closes #120) 2021-03-13 01:48:17 +01:00
codl f10b9dac80 brotli: degrade gracefully when redis is missing 2021-03-13 01:17:21 +01:00
dependabot[bot] 785d0509af Bump alembic from 1.5.6 to 1.5.7
Bumps [alembic](https://alembic.sqlalchemy.org) from 1.5.6 to 1.5.7.

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-12 23:59:03 +01:00
dependabot[bot] 2c13e2f167
Merge pull request #416 from codl/dependabot/pip/pytest-cov-2.11.1 2021-03-11 13:15:58 +00:00
dependabot[bot] 7c1b42c7d0
Merge pull request #415 from codl/dependabot/npm_and_yarn/rollup-2.41.1 2021-03-11 12:31:26 +00:00
dependabot[bot] bdfae33102
Bump pytest-cov from 2.10.1 to 2.11.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.10.1 to 2.11.1.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.10.1...v2.11.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-11 05:29:37 +00:00
dependabot[bot] 4b3ffe976b
Bump rollup from 2.41.0 to 2.41.1
Bumps [rollup](https://github.com/rollup/rollup) from 2.41.0 to 2.41.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.41.0...v2.41.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-11 05:22:49 +00:00
dependabot[bot] 4c82298998 Bump coverage from 5.1 to 5.5
Bumps [coverage](https://github.com/nedbat/coveragepy) from 5.1 to 5.5.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-5.1...coverage-5.5)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-10 12:41:23 +01:00
dependabot[bot] f763184ead Bump rollup from 2.40.0 to 2.41.0
Bumps [rollup](https://github.com/rollup/rollup) from 2.40.0 to 2.41.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.40.0...v2.41.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-10 12:40:35 +01:00
codl e274999c2c
Merge pull request #410 from codl/dependabot/pip/versioneer-0.19
Bump versioneer from 0.18 to 0.19
2021-03-10 05:50:56 +01:00
codl 242ec23e2d update in-tree versioneer 2021-03-10 05:44:47 +01:00
dependabot[bot] dc5eba6ed5
Merge pull request #412 from codl/dependabot/pip/psycopg2-2.8.6 2021-03-09 09:28:42 +00:00
dependabot[bot] 7e9aeb7c59
Bump psycopg2 from 2.8.5 to 2.8.6
Bumps [psycopg2](https://github.com/psycopg/psycopg2) from 2.8.5 to 2.8.6.
- [Release notes](https://github.com/psycopg/psycopg2/releases)
- [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS)
- [Commits](https://github.com/psycopg/psycopg2/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 09:13:25 +00:00
dependabot[bot] 3dc188e4d2
Merge pull request #403 from codl/dependabot/pip/sqlalchemy-1.3.23 2021-03-09 09:10:50 +00:00
dependabot[bot] f6d1e62b01
Bump sqlalchemy from 1.3.17 to 1.3.23
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.17 to 1.3.23.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 08:58:19 +00:00
codl 3752fb168a
update maintained badge 2021-03-09 09:57:14 +01:00
dependabot[bot] a43cf77195 Bump alembic from 1.4.2 to 1.5.6
Bumps [alembic](https://alembic.sqlalchemy.org) from 1.4.2 to 1.5.6.

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 09:56:31 +01:00
dependabot[bot] 2352ed84ac
Merge pull request #408 from codl/dependabot/pip/doit-0.33.1 2021-03-09 08:50:45 +00:00
dependabot[bot] 22a2c3a70c
Bump versioneer from 0.18 to 0.19
Bumps [versioneer](https://github.com/python-versioneer/python-versioneer) from 0.18 to 0.19.
- [Release notes](https://github.com/python-versioneer/python-versioneer/releases)
- [Changelog](https://github.com/python-versioneer/python-versioneer/blob/master/NEWS.md)
- [Commits](https://github.com/python-versioneer/python-versioneer/compare/0.18...0.19)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 08:47:10 +00:00
dependabot[bot] cd613259c8
Merge pull request #409 from codl/dependabot/pip/brotli-1.0.9 2021-03-09 08:46:02 +00:00
dependabot[bot] c760178ba9
Bump doit from 0.32.0 to 0.33.1
Bumps [doit](https://github.com/pydoit/doit) from 0.32.0 to 0.33.1.
- [Release notes](https://github.com/pydoit/doit/releases)
- [Changelog](https://github.com/pydoit/doit/blob/master/CHANGES)
- [Commits](https://github.com/pydoit/doit/compare/0.32.0...0.33.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 08:36:18 +00:00
dependabot[bot] 1ea6d13bf0
Bump brotli from 1.0.7 to 1.0.9
Bumps [brotli](https://github.com/google/brotli) from 1.0.7 to 1.0.9.
- [Release notes](https://github.com/google/brotli/releases)
- [Commits](https://github.com/google/brotli/compare/v1.0.7...v1.0.9)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 08:36:03 +00:00
dependabot[bot] bf6068f5d9 Bump flask-sqlalchemy from 2.4.3 to 2.4.4
Bumps [flask-sqlalchemy](https://github.com/pallets/flask-sqlalchemy) from 2.4.3 to 2.4.4.
- [Release notes](https://github.com/pallets/flask-sqlalchemy/releases)
- [Changelog](https://github.com/pallets/flask-sqlalchemy/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/flask-sqlalchemy/compare/2.4.3...2.4.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 09:29:19 +01:00
dependabot[bot] a8350cbf6f Bump svelte from 3.34.0 to 3.35.0
Bumps [svelte](https://github.com/sveltejs/svelte) from 3.34.0 to 3.35.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v3.34.0...v3.35.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 09:25:35 +01:00
dependabot[bot] 297d4c9d94 Bump pillow from 7.1.2 to 8.1.2
Bumps [pillow](https://github.com/python-pillow/Pillow) from 7.1.2 to 8.1.2.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/7.1.2...8.1.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 08:59:20 +01:00
codl 5866592f50 update dependabot settings 2021-03-09 01:12:22 +01:00
dependabot[bot] 1a4e3c10e6 Bump requests from 2.23.0 to 2.25.1
Bumps [requests](https://github.com/psf/requests) from 2.23.0 to 2.25.1.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.23.0...v2.25.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-09 01:06:51 +01:00
codl ab5e27447d fix svelte compile 2021-03-09 00:08:45 +01:00
codl 2f9db993dc travis: update python matrix to include 3.9 2021-03-08 23:43:24 +01:00
codl 9b42bb4bf0 this author does not recommend caddy anymore
no hard feelings i just never could transition to 2.X
2021-03-08 23:33:04 +01:00
dependabot[bot] 095a612473
Merge pull request #391 from codl/dependabot/pip/pytest-6.2.2 2021-03-08 22:00:20 +00:00
dependabot[bot] 47b3384778
Bump pytest from 5.4.2 to 6.2.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.2 to 6.2.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.4.2...6.2.2)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 21:59:15 +00:00
dependabot-preview[bot] ccbe71e650 Bump flask-sqlalchemy from 2.4.1 to 2.4.3
Bumps [flask-sqlalchemy](https://github.com/pallets/flask-sqlalchemy) from 2.4.1 to 2.4.3.
- [Release notes](https://github.com/pallets/flask-sqlalchemy/releases)
- [Changelog](https://github.com/pallets/flask-sqlalchemy/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/flask-sqlalchemy/compare/2.4.1...2.4.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2021-03-08 22:55:50 +01:00
dependabot[bot] ece2b33a73
Merge pull request #402 from codl/dependabot/npm_and_yarn/rollup-2.40.0 2021-03-08 21:54:50 +00:00
dependabot[bot] e7140727fa
Merge pull request #394 from codl/dependabot/npm_and_yarn/rollup-plugin-svelte-7.1.0 2021-03-08 21:54:41 +00:00
dependabot[bot] dbdfe05950 Bump redis from 3.4.1 to 3.5.3
Bumps [redis](https://github.com/andymccurdy/redis-py) from 3.4.1 to 3.5.3.
- [Release notes](https://github.com/andymccurdy/redis-py/releases)
- [Changelog](https://github.com/andymccurdy/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/andymccurdy/redis-py/compare/3.4.1...3.5.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 22:54:12 +01:00
dependabot[bot] 4fe30a38a9
Bump rollup from 2.12.0 to 2.40.0
Bumps [rollup](https://github.com/rollup/rollup) from 2.12.0 to 2.40.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.12.0...v2.40.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 21:53:31 +00:00
dependabot[bot] e7774659de
Bump rollup-plugin-svelte from 5.1.1 to 7.1.0
Bumps [rollup-plugin-svelte](https://github.com/sveltejs/rollup-plugin-svelte) from 5.1.1 to 7.1.0.
- [Release notes](https://github.com/sveltejs/rollup-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/rollup-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/rollup-plugin-svelte/compare/v5.1.1...v7.1.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 21:53:13 +00:00
dependabot[bot] 66b7cd7ab8 Bump svelte from 3.6.10 to 3.34.0
Bumps [svelte](https://github.com/sveltejs/svelte) from 3.6.10 to 3.34.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v3.6.10...v3.34.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-08 22:52:36 +01:00
codl 3f7f0f7f2f
Merge branch 'master' into copyedit 2021-03-08 22:35:22 +01:00
dependabot[bot] 2672f17d73 Bump codecov from 2.1.3 to 2.1.11
Bumps [codecov](https://github.com/codecov/codecov-python) from 2.1.3 to 2.1.11.
- [Release notes](https://github.com/codecov/codecov-python/releases)
- [Changelog](https://github.com/codecov/codecov-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-python/commits/v2.1.11)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-26 18:48:41 +01:00
dependabot[bot] 0234217c64 Bump pytest-cov from 2.9.0 to 2.10.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.9.0 to 2.10.1.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.9.0...v2.10.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-01-26 10:02:38 +01:00
codl ad56e4720a update changelog 2020-12-29 22:09:45 +01:00
codl 162f798df7 simplify (and hand-write) query in refresh_account_with_oldest_post.
.#166

this takes about 1/5th the time of the old query. it's still kinda slow
2020-12-29 21:49:27 +01:00
codl ed9146231e add proxyfix 2020-11-18 01:23:58 +01:00
codl 57c0752994
update changelog 2020-09-18 17:00:09 +02:00
codl d2c3d95b1e
copyedit on settings form. widened page 2020-09-18 00:17:00 +02:00
codl 48b9ec7796
tweak dependabot settings 2020-06-27 22:36:07 +02:00
dependabot-preview[bot] 8d256e1756 Create Dependabot config file 2020-06-27 22:14:44 +02:00
codl 92cce06ff1 Create FUNDING.yml
cough
2020-06-03 06:52:15 +02:00
dependabot-preview[bot] 542d0fb35b Bump pytest from 5.4.1 to 5.4.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.1 to 5.4.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.4.1...5.4.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-01 20:29:29 +00:00
dependabot-preview[bot] 65ac5440c7 Bump pytest-cov from 2.8.1 to 2.9.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.8.1 to 2.9.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.8.1...v2.9.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-01 20:21:27 +00:00
dependabot-preview[bot] 4d4a151ce6 Bump sqlalchemy from 1.3.16 to 1.3.17
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.16 to 1.3.17.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-01 20:17:12 +00:00
dependabot-preview[bot] 80601ae42e Bump pillow from 7.1.1 to 7.1.2
Bumps [pillow](https://github.com/python-pillow/Pillow) from 7.1.1 to 7.1.2.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/7.1.1...7.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-01 22:11:06 +02:00
codl 16c02bb198
empty commit to jostle CI 2020-06-01 21:58:18 +02:00
dependabot-preview[bot] d17cf789e4
Bump codecov from 2.0.22 to 2.1.3
Bumps [codecov](https://github.com/codecov/codecov-python) from 2.0.22 to 2.1.3.
- [Release notes](https://github.com/codecov/codecov-python/releases)
- [Changelog](https://github.com/codecov/codecov-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-python/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-01 14:36:43 +00:00
dependabot-preview[bot] 560283caa2 Bump rollup from 1.17.0 to 2.12.0
Bumps [rollup](https://github.com/rollup/rollup) from 1.17.0 to 2.12.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v1.17.0...v2.12.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-06-01 12:30:11 +00:00
codl d1f02513d6 remove python 3.9 beta from travis build matrix
idk why it breaks pillow, and i can't reproduce it outside of travis, so
i'm going to assume this is not my bug
2020-06-01 14:24:04 +02:00
codl 5cf0efe05d README: specify node version
weird that it didn't even say node at all
2020-06-01 14:24:04 +02:00
codl eabd4740ad travis: update versions, force travis to use node 10 2020-06-01 14:24:04 +02:00
codl 4540d87038
add acknowledgements to readme 2020-06-01 03:51:55 +02:00
codl 2e2e915b0a remove yarn from readme requirements
yarn probably still works, but i don't use it and don't follow its
development so i can't say i support its use
2020-05-24 22:36:00 +02:00
codl 73ce05d9b7
pipenv lock 2020-04-25 07:53:26 +02:00
codl 25dcf17f54 example config: mention twitter callback url
also added a note to uncomment values before editing them, just to be safe
2020-03-11 22:36:18 +01:00
codl cdd7d43a18 readme: update maintained badge 2020-01-03 01:22:53 +01:00
dependabot-preview[bot] 4f0bae9a74 Bump rollup-plugin-svelte from 5.1.0 to 5.1.1
Bumps [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/rollup/rollup-plugin-svelte/releases)
- [Changelog](https://github.com/sveltejs/rollup-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-svelte/compare/v5.1.0...v5.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-12-08 12:59:20 +01:00
codl 6c8d20b87e
update changelog and release v2.0.0 2019-09-13 03:45:56 +02:00
codl 9592ab8511
disable archives
it has come to my attention while investigating #263 that tweet archives
as forget supports them are no longer a thing. this commit disables all
front-facing archive support and puts a banner above the form
2019-09-13 03:33:39 +02:00
codl c5c4b72c6f
ensure fetch_acc is rescheduled properly when doing historic fetch
the unique flag on the task means we can't reschedule it for right now
but we must wait for the current one to end. this commit introduces a 1
second delay before the task is ran again, which should be enough
2019-09-13 03:07:58 +02:00
dependabot-preview[bot] 741a44bed8 Bump alembic from 1.0.10 to 1.1.0
Bumps [alembic](https://github.com/sqlalchemy/alembic) from 1.0.10 to 1.1.0.
- [Release notes](https://github.com/sqlalchemy/alembic/releases)
- [Changelog](https://github.com/sqlalchemy/alembic/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/alembic/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-28 15:06:30 +00:00
dependabot-preview[bot] 19dc13bc93 Bump pytest from 5.0.0 to 5.1.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.0.0 to 5.1.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.0.0...5.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-28 15:03:28 +00:00
dependabot-preview[bot] e369b554b1 Bump flask from 1.0.3 to 1.1.1
Bumps [flask](https://github.com/pallets/flask) from 1.0.3 to 1.1.1.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/1.0.3...1.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-28 10:23:43 +00:00
dependabot-preview[bot] ac4d143d7f Bump coverage from 4.5.3 to 4.5.4
Bumps [coverage](https://github.com/nedbat/coveragepy) from 4.5.3 to 4.5.4.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-4.5.3...coverage-4.5.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-28 10:17:47 +00:00
dependabot-preview[bot] 1668d1cd2d Bump svelte from 3.1.0 to 3.6.10
Bumps [svelte](https://github.com/sveltejs/svelte) from 3.1.0 to 3.6.10.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v3.1.0...v3.6.10)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-28 12:11:41 +02:00
dependabot-preview[bot] 09f7127e52 Bump pillow from 6.0.0 to 6.1.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 6.0.0 to 6.1.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/6.0.0...6.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-28 12:11:27 +02:00
codl 249842ed9f
fix newer twitter accounts not fetching new posts after the initial fetch
twitter returns the post on the max_id boundary, which mastodon does not
do. the code that decides to move from historical fetch to a new batch
assumes that no posts will be returned at all when we're done, but in
twitter's case that will never happen, or not until the oldest post is
deleted. this change updates that code to ignore any posts returned that
match either max_id or since_id
2019-08-28 11:32:19 +02:00
codl 387b287990
release v1.6.1 2019-07-23 04:50:20 +02:00
codl 195371dc97
forgot to also update the log in button template for known instances js 2019-07-23 04:42:44 +02:00
codl 289a1df83f
color login buttons in css instead of hardcoding colors into the html 2019-07-23 04:32:19 +02:00
codl fb1725d43a
new, better about page
I've long wanted to get rid of this weirdly negative and tongue in cheek
about page
2019-07-23 04:10:33 +02:00
codl 38339defba
update changelog with earlier changes 2019-07-23 02:28:42 +02:00
codl ac76dd4ad1
make version link in footer link to changelog, not commit log 2019-07-23 02:17:01 +02:00
codl f01b5b1511
guhhhhhhh. i forgot an import 2019-07-19 05:48:43 +02:00
dependabot-preview[bot] cd5ac7a52e Bump rollup-plugin-svelte from 5.0.3 to 5.1.0
Bumps [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) from 5.0.3 to 5.1.0.
- [Release notes](https://github.com/rollup/rollup-plugin-svelte/releases)
- [Changelog](https://github.com/rollup/rollup-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-svelte/compare/v5.0.3...v5.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-19 00:51:09 +00:00
dependabot-preview[bot] cf2ebe22a0 Bump rollup from 1.13.1 to 1.17.0
Bumps [rollup](https://github.com/rollup/rollup) from 1.13.1 to 1.17.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v1.13.1...v1.17.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-19 00:40:43 +00:00
dependabot-preview[bot] da8fb2b4b5 Bump psycopg2 from 2.8.2 to 2.8.3
Bumps [psycopg2](https://github.com/psycopg/psycopg2) from 2.8.2 to 2.8.3.
- [Release notes](https://github.com/psycopg/psycopg2/releases)
- [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS)
- [Commits](https://github.com/psycopg/psycopg2/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-18 14:45:43 +02:00
dependabot-preview[bot] 0386f55f7b Bump sqlalchemy from 1.3.3 to 1.3.5
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.3 to 1.3.5.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-18 14:45:24 +02:00
dependabot-preview[bot] fe765165da Bump rollup-plugin-node-resolve from 4.2.4 to 5.2.0
Bumps [rollup-plugin-node-resolve](https://github.com/rollup/rollup-plugin-node-resolve) from 4.2.4 to 5.2.0.
- [Release notes](https://github.com/rollup/rollup-plugin-node-resolve/releases)
- [Changelog](https://github.com/rollup/rollup-plugin-node-resolve/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-node-resolve/compare/v4.2.4...v5.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-18 02:19:27 +00:00
dependabot-preview[bot] 45f70c1132 Bump pytest from 4.6.1 to 5.0.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 4.6.1 to 5.0.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/4.6.1...5.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-18 01:13:15 +02:00
codl 7fad5ea458 shorten frequency at which we refresh the most stale posts 2019-07-16 18:50:25 +00:00
codl 6a597bf53a
v1.5.3 2019-07-11 22:44:30 +02:00
codl 9eba78c125
typo 😩 this is what happens when i try to fix things fast 2019-07-11 22:43:32 +02:00
codl 53113ce18a
v1.5.2 2019-07-11 22:36:42 +02:00
codl da7d64acbd
set user agent on mastodon.py requests 2019-07-11 22:33:14 +02:00
dependabot-preview[bot] 9e3e68de3a Bump requests from 2.21.0 to 2.22.0
Bumps [requests](https://github.com/requests/requests) from 2.21.0 to 2.22.0.
- [Release notes](https://github.com/requests/requests/releases)
- [Changelog](https://github.com/kennethreitz/requests/blob/master/HISTORY.md)
- [Commits](https://github.com/requests/requests/compare/v2.21.0...v2.22.0)
2019-06-03 18:09:48 +00:00
dependabot-preview[bot] af17d5c343 Bump pytest-cov from 2.6.1 to 2.7.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.6.1 to 2.7.1.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.6.1...v2.7.1)
2019-06-03 18:08:42 +00:00
dependabot-preview[bot] 941d017b1d Bump pytest from 4.4.1 to 4.6.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 4.4.1 to 4.6.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/4.4.1...4.6.1)
2019-06-03 19:57:57 +02:00
dependabot-preview[bot] 6625ae20d1 Bump flask-migrate from 2.4.0 to 2.5.2
Bumps [flask-migrate](https://github.com/miguelgrinberg/flask-migrate) from 2.4.0 to 2.5.2.
- [Release notes](https://github.com/miguelgrinberg/flask-migrate/releases)
- [Changelog](https://github.com/miguelgrinberg/Flask-Migrate/blob/master/CHANGES.md)
- [Commits](https://github.com/miguelgrinberg/flask-migrate/compare/v2.4.0...v2.5.2)
2019-06-03 12:45:40 +02:00
dependabot-preview[bot] e64a526c73 Bump flask from 1.0.2 to 1.0.3
Bumps [flask](https://github.com/pallets/flask) from 1.0.2 to 1.0.3.
- [Release notes](https://github.com/pallets/flask/releases)
- [Changelog](https://github.com/pallets/flask/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/flask/compare/1.0.2...1.0.3)
2019-06-03 12:45:10 +02:00
dependabot-preview[bot] 2112d31298 Bump rollup from 1.10.1 to 1.13.1
Bumps [rollup](https://github.com/rollup/rollup) from 1.10.1 to 1.13.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v1.10.1...v1.13.1)
2019-06-02 11:38:51 +00:00
dependabot-preview[bot] ed06923024 Bump rollup-plugin-node-resolve from 3.4.0 to 4.2.4
Bumps [rollup-plugin-node-resolve](https://github.com/rollup/rollup-plugin-node-resolve) from 3.4.0 to 4.2.4.
- [Release notes](https://github.com/rollup/rollup-plugin-node-resolve/releases)
- [Changelog](https://github.com/rollup/rollup-plugin-node-resolve/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-node-resolve/compare/v3.4.0...v4.2.4)
2019-06-02 09:20:08 +00:00
codl 2f3efe8ca0
Merge pull request #202 from codl/ignore-more
ignore posts that are not known to match policy
2019-05-11 20:32:32 +02:00
codl 36860b6bf7
ignore posts that are not known to match policy
until now, delete_from_account would only ignore posts that were too
young, but would fetch and refresh posts that didn't match
fave/media/DM policies. this was fine most of the time but would result
in a lot of refreshes if those policies were very restrictive. now these
policies are respected when selecting candidate posts for deletion

closes #174
2019-05-11 20:21:43 +02:00
codl dea3ab760a
Merge pull request #200 from codl/release
release v1.5.1
2019-05-02 02:08:30 +02:00
codl c7858d6121
travis: cache npm packages as well as python ones. take two 2019-05-02 02:02:27 +02:00
codl 5e5d0fb5ef
release v1.5.1 2019-05-02 01:49:13 +02:00
codl 733bb8751a
travis: cache npm packages as well as python ones 2019-05-02 01:44:28 +02:00
dependabot[bot] 8f3b9e9aa3 Bump alembic from 1.0.7 to 1.0.10
Bumps [alembic](https://alembic.sqlalchemy.org) from 1.0.7 to 1.0.10.

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 23:39:44 +00:00
dependabot[bot] 66ca143e68 Bump sqlalchemy from 1.2.18 to 1.3.3
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.2.18 to 1.3.3.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 23:39:28 +00:00
dependabot[bot] cd114cf7e2 Bump psycopg2 from 2.7.7 to 2.8.2
Bumps [psycopg2](https://github.com/psycopg/psycopg2) from 2.7.7 to 2.8.2.
- [Release notes](https://github.com/psycopg/psycopg2/releases)
- [Changelog](https://github.com/psycopg/psycopg2/blob/master/NEWS)
- [Commits](https://github.com/psycopg/psycopg2/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 23:32:12 +00:00
dependabot[bot] afbb7e76fe Bump flask-sqlalchemy from 2.3.2 to 2.4.0
Bumps [flask-sqlalchemy](https://github.com/pallets/flask-sqlalchemy) from 2.3.2 to 2.4.0.
- [Release notes](https://github.com/pallets/flask-sqlalchemy/releases)
- [Changelog](https://github.com/pallets/flask-sqlalchemy/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/flask-sqlalchemy/compare/2.3.2...2.4.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 23:31:08 +00:00
dependabot[bot] fa425d7ad9
Bump celery from 4.2.1 to 4.3.0
Bumps [celery](https://github.com/celery/celery) from 4.2.1 to 4.3.0.
- [Release notes](https://github.com/celery/celery/releases)
- [Changelog](https://github.com/celery/celery/blob/master/Changelog)
- [Commits](https://github.com/celery/celery/compare/v4.2.1...v4.3.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 23:25:48 +00:00
dependabot[bot] a16c0e9f52 Bump pillow from 5.4.1 to 6.0.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 5.4.1 to 6.0.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/5.4.1...6.0.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 23:19:08 +00:00
dependabot[bot] e94cf35165 Bump rollup from 1.3.3 to 1.10.1
Bumps [rollup](https://github.com/rollup/rollup) from 1.3.3 to 1.10.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v1.3.3...v1.10.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 23:18:56 +00:00
dependabot[bot] b4bacd271e Bump redis from 3.2.0 to 3.2.1
Bumps [redis](https://github.com/andymccurdy/redis-py) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/andymccurdy/redis-py/releases)
- [Changelog](https://github.com/andymccurdy/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/andymccurdy/redis-py/compare/3.2.0...3.2.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 23:18:19 +00:00
codl 1374dda768
Merge pull request #198 from codl/fix-imgproxy
fix broken imgproxy urls
2019-05-02 01:11:05 +02:00
codl 796a78dc2e
fix broken imgproxy urls
idk why this worked on python 3.5 but it doesn't work on 3.7
2019-05-02 00:57:45 +02:00
codl 1d57bb23c3
update to svelte 3 2019-05-02 00:47:44 +02:00
codl 0f13f3bd29
Merge pull request #192 from codl/dependabot/pip/mastodon-py-1.4.0
Bump mastodon-py from 1.3.1 to 1.4.0
2019-05-01 10:37:58 +02:00
dependabot[bot] 6f6f6f8d6b
Bump mastodon-py from 1.3.1 to 1.4.0
Bumps [mastodon-py](https://github.com/halcy/Mastodon.py) from 1.3.1 to 1.4.0.
- [Release notes](https://github.com/halcy/Mastodon.py/releases)
- [Changelog](https://github.com/halcy/Mastodon.py/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/halcy/Mastodon.py/compare/1.3.1...1.4.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-05-01 06:52:53 +00:00
dependabot[bot] 1751dd579a Bump pytest from 4.3.0 to 4.4.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 4.3.0 to 4.4.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/4.3.0...4.4.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-04-19 20:32:36 +00:00
dependabot[bot] 29b6847104 Bump coverage from 4.5.2 to 4.5.3
Bumps [coverage](https://github.com/nedbat/coveragepy) from 4.5.2 to 4.5.3.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/coverage-4.5.3/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/coverage-4.5.2...coverage-4.5.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-04-19 20:26:19 +00:00
dependabot[bot] 4fa26dc3c7 [Security] Bump urllib3 from 1.24.1 to 1.24.2
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.24.1 to 1.24.2. **This update includes security fixes.**
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.24.1...1.24.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-04-19 20:23:56 +00:00
dependabot[bot] 6902f9a7d8 [Security] Bump jinja2 from 2.10 to 2.10.1
Bumps [jinja2](https://github.com/mitsuhiko/jinja2) from 2.10 to 2.10.1. **This update includes security fixes.**
- [Release notes](https://github.com/mitsuhiko/jinja2/releases)
- [Changelog](https://github.com/pallets/jinja/blob/master/docs/changelog.rst)
- [Commits](https://github.com/mitsuhiko/jinja2/compare/2.10...2.10.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-04-13 19:07:06 +00:00
codl 93b0f2c11d
release 1.5.0 2019-03-15 21:53:12 +01:00
codl 8b1af6ecb6
Merge pull request #176 from codl/175
move known instance buttons entirely client-side
2019-03-15 21:50:36 +01:00
codl a00bbe0e14
update changelog 2019-03-15 21:37:14 +01:00
codl ab4cc996ab
move normalize to the more general known_instances.js 2019-03-15 21:28:41 +01:00
codl bd795157c7
fix duplicate instances between known and top 2019-03-15 21:23:20 +01:00
codl b4c332190e
return 404 on empty known instances cookie
that way it will use the default
2019-03-15 21:21:56 +01:00
codl ca5ccada19
remove stray console.logs 2019-03-15 21:15:55 +01:00
codl 2bacbaa8b1
known instances: bump instance counter when logging in 2019-03-15 21:09:22 +01:00
codl 915a6029d7
port algorithm for normalizing known instances to five visible slots 2019-03-15 20:25:45 +01:00
codl 17f59a018f
script to show known instances from cookie, mmoving it to localstorage 2019-03-15 19:46:18 +01:00
codl ec10d15217
pad to avoid oracle attacks on /api/known_instances 2019-03-15 18:29:55 +01:00
codl 8cca6c2fe3
add templates for instance buttons 2019-03-15 17:59:44 +01:00
codl 8cf12f31c8
add endpoint to access and clear existing known instances cookie 2019-03-15 17:18:43 +01:00
codl b57f71ae58
remove known instances 2019-03-15 17:17:00 +01:00
codl 41683fcffd
Merge pull request #172 from codl/avoid-limit
mastodon: stop before hitting the rate limit
2019-03-11 16:40:33 +01:00
codl 843825b1d9
update changelog 2019-03-11 16:37:47 +01:00
codl a68a673925
mastodon: stop before hitting the rate limit 2019-03-11 16:35:43 +01:00
codl 689c013f55
Merge pull request #173 from codl/clean-tasks
make fetch_acc unique, various task cleanup
2019-03-11 16:33:48 +01:00
codl 27df3e1a51
update changelog 2019-03-11 16:25:15 +01:00
codl 2b39a61442
increased task frequency for refreshes. decreased for bookkeeping 2019-03-11 12:18:51 +01:00
codl 0f3df6ad24
remove leftover debug print, standardise task prints 2019-03-11 12:12:04 +01:00
codl 249fab7014
make fetch_acc unique
now that it doesn't carry state info in its arguments, it's safe to skip
dupes
2019-03-11 12:08:47 +01:00
codl 095952f767
update changelog. release v1.4.3 2019-03-11 03:31:04 +01:00
codl fc06355bca
Merge pull request #170 from codl/fix-fetch-acc
fix crash in fetch_acc when user has no posts
2019-03-11 03:19:14 +01:00
codl 1430361763
Merge pull request #169 from codl/fix-deadlock
disable autoflush in refresh_posts, fixing deadlocks. closes GH-19
2019-03-11 03:18:58 +01:00
codl 7647ca86fc
Merge pull request #168 from codl/crash-backoff
refresh_account: increment backoff if something crashes
2019-03-11 03:17:57 +01:00
codl 6d6184f3d8
disable autoflush in refresh_posts, fixing deadlocks. closes GH-19 2019-03-11 03:10:32 +01:00
codl 84089f8a40
refresh_account: increment backoff if something crashes
this is kind of a hotfix for the thing with pleroma returning bad error
messages crashing mastodon.py. but it's good practice anyway
2019-03-11 03:10:06 +01:00
codl 84e0ad6b1f
fix crash in fetch_acc when user has no posts 2019-03-09 15:56:07 +01:00
codl 78c84ed92c
Merge branch 'existenz' 2019-03-08 01:27:23 +01:00
codl dfcc9287b8
check for existence before refreshing oldest post/account
this only happens when you don't have any accounts yet and it is a bad
look the first time you set up a new forget install
2019-03-08 01:24:13 +01:00
codl 280a5bee3f cough. nothing to see here [skip ci] 2019-03-08 01:09:41 +01:00
codl 89a718cfea document config.example.py better 2019-03-08 01:02:25 +01:00
codl 445caf1daa Update README.markdown
npm ci only exists in recent-ish versions of npm. replaced with npm install
2019-03-07 12:12:21 +01:00
dependabot[bot] eae4d06f42 Bump rollup from 1.2.3 to 1.3.3
Bumps [rollup](https://github.com/rollup/rollup) from 1.2.3 to 1.3.3.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v1.2.3...v1.3.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-03-04 03:02:02 +00:00
codl 44474f096d
update promo screenshot 2019-02-25 01:46:46 +01:00
codl fa832412fa
readme: update maintenance badge 2019-02-25 01:07:59 +01:00
codl 316e747b9b
update requirements files
oops i forgot
2019-02-25 00:57:10 +01:00
codl ce9feb0e0e
release 1.4.2 2019-02-24 16:45:51 +01:00
codl 61131f4298
Merge branch 'fix-refresh' 2019-02-24 16:36:23 +01:00
codl 4054ebfffd
Merge branch 'master' into fix-refresh 2019-02-24 16:35:44 +01:00
codl b3168278d3
Merge branch 'new-fetch' 2019-02-24 16:32:27 +01:00
codl 34814b4661
update changelog 2019-02-24 16:31:57 +01:00
codl d5e0b43c9e
fix: posts not getting refreshed
turns out i was not touching updated_at, so i'd always be refreshing
the same posts unless one of them actually changed

this is embarassing, there are posts in the DB that haven't been
refreshed since 2017
2019-02-24 16:28:59 +01:00
codl b4ce1964f5
update changelog 2019-02-24 15:40:41 +01:00
codl a6c5361138
more robust fetching. closes #13
this also refactors libforget so neither the twitter or mastodon lib
insert posts directly
2019-02-24 15:40:41 +01:00
codl 649b68793c
libforget mastodon: same thing. raise temporaryerror if no access 2019-02-24 13:11:31 +01:00
codl 60e2357597
twitter: raise temporaryerror when no access instead of silently failing 2019-02-24 12:48:58 +01:00
codl 68ac747f7e
Merge remote-tracking branch 'github/master' into update-more-deps 2019-02-24 11:05:23 +01:00
dependabot[bot] 8ae7338652 Bump rollup from 0.66.6 to 1.2.3
Bumps [rollup](https://github.com/rollup/rollup) from 0.66.6 to 1.2.3.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v0.66.6...v1.2.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-24 00:08:11 +00:00
codl d01b85b584
update more deps 2019-02-24 00:55:18 +01:00
dependabot[bot] cfd2318b2c Bump rollup-plugin-svelte from 4.3.2 to 5.0.3
Bumps [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) from 4.3.2 to 5.0.3.
- [Release notes](https://github.com/rollup/rollup-plugin-svelte/releases)
- [Changelog](https://github.com/rollup/rollup-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-svelte/compare/v4.3.2...v5.0.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-23 23:50:36 +00:00
codl e2cef52eb1
update changelog 2019-02-24 00:44:47 +01:00
codl 4d001f31f3
Merge branch 'update-deps' 2019-02-24 00:42:40 +01:00
dependabot[bot] c42265070b Bump coverage from 4.5.1 to 4.5.2
Bumps [coverage](https://bitbucket.org/ned/coveragepy) from 4.5.1 to 4.5.2.
- [Changelog](https://bitbucket.org/ned/coveragepy/src/default/CHANGES.rst)
- [Commits](https://bitbucket.org/ned/coveragepy/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-23 23:41:09 +00:00
codl e9749f70e0
no more florps. that joke is getting dated 2019-02-24 00:40:28 +01:00
codl fc25e6ca7b
fix picture tags having an extra comma in their srcset 2019-02-24 00:40:25 +01:00
dependabot[bot] af3a51ae19 Bump svelte from 2.15.0 to 2.16.1
Bumps [svelte](https://github.com/sveltejs/svelte) from 2.15.0 to 2.16.1.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/v2.16.1/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v2.15.0...v2.16.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-23 23:29:04 +00:00
dependabot[bot] bff6df0314 Bump raven from 6.9.0 to 6.10.0
Bumps [raven](https://github.com/getsentry/raven-python) from 6.9.0 to 6.10.0.
- [Release notes](https://github.com/getsentry/raven-python/releases)
- [Changelog](https://github.com/getsentry/raven-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/raven-python/compare/6.9.0...6.10.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-23 23:16:56 +00:00
dependabot[bot] 6266a1d603 Bump requests from 2.20.0 to 2.21.0
Bumps [requests](https://github.com/requests/requests) from 2.20.0 to 2.21.0.
- [Release notes](https://github.com/requests/requests/releases)
- [Changelog](https://github.com/requests/requests/blob/master/HISTORY.md)
- [Commits](https://github.com/requests/requests/compare/v2.20.0...v2.21.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-23 22:58:49 +00:00
dependabot[bot] 6355addcfb Bump redis from 2.10.6 to 3.0.1
Bumps [redis](https://github.com/andymccurdy/redis-py) from 2.10.6 to 3.0.1.
- [Release notes](https://github.com/andymccurdy/redis-py/releases)
- [Changelog](https://github.com/andymccurdy/redis-py/blob/master/CHANGES)
- [Commits](https://github.com/andymccurdy/redis-py/compare/2.10.6...3.0.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2019-02-23 22:37:09 +00:00
codl 0cb94cfa01 remove x- prefix from headers 2018-11-30 23:04:37 +01:00
codl 74d04ce7aa
release v1.4.1 2018-10-29 22:07:48 +01:00
codl 170c535928
Merge remote-tracking branch 'github/master' 2018-10-29 22:01:53 +01:00
dependabot[bot] e647e5322a Bump pytest from 3.8.2 to 3.9.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 3.8.2 to 3.9.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/3.8.2...3.9.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-29 20:57:40 +00:00
dependabot[bot] f5f404b3d1 Bump rollup from 0.66.4 to 0.66.6
Bumps [rollup](https://github.com/rollup/rollup) from 0.66.4 to 0.66.6.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v0.66.4...v0.66.6)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-29 20:49:37 +00:00
dependabot[bot] 3f0a5a193e Bump svelte from 2.13.5 to 2.15.0
Bumps [svelte](https://github.com/sveltejs/svelte) from 2.13.5 to 2.15.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v2.13.5...v2.15.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-29 20:40:03 +00:00
dependabot[bot] 536574df8b Bump flask-migrate from 2.2.1 to 2.3.0
Bumps [flask-migrate](https://github.com/miguelgrinberg/flask-migrate) from 2.2.1 to 2.3.0.
- [Release notes](https://github.com/miguelgrinberg/flask-migrate/releases)
- [Changelog](https://github.com/miguelgrinberg/Flask-Migrate/blob/master/CHANGELOG.md)
- [Commits](https://github.com/miguelgrinberg/flask-migrate/compare/v2.2.1...v2.3.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-29 20:26:56 +00:00
dependabot[bot] 71a6d52303 Bump brotli from 1.0.6 to 1.0.7
Bumps [brotli](https://github.com/google/brotli) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/google/brotli/releases)
- [Commits](https://github.com/google/brotli/compare/v1.0.6...v1.0.7)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-29 20:16:23 +00:00
dependabot[bot] aa8011f72e Bump alembic from 1.0.0 to 1.0.1
Bumps [alembic](https://bitbucket.org/zzzeek/alembic) from 1.0.0 to 1.0.1.
- [Changelog](https://bitbucket.org/zzzeek/alembic/src/master/CHANGES)
- [Commits](https://bitbucket.org/zzzeek/alembic/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-29 20:01:24 +00:00
codl dcd12407e5
update changelog 2018-10-29 21:00:18 +01:00
dependabot[bot] d8b8ca6612 Bump requests from 2.19.1 to 2.20.0
Bumps [requests](https://github.com/requests/requests) from 2.19.1 to 2.20.0.
- [Release notes](https://github.com/requests/requests/releases)
- [Changelog](https://github.com/requests/requests/blob/master/HISTORY.md)
- [Commits](https://github.com/requests/requests/compare/v2.19.1...v2.20.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-29 19:51:53 +00:00
codl 34fa0c7f57
release v1.4.0 2018-10-06 02:21:41 +02:00
codl 4363a8d7bd
Merge branch 'master' into remove-yarn 2018-10-06 02:18:44 +02:00
codl 4147673915
Merge pull request #92 from codl/archive-warn
warn when archive looks too big
2018-10-06 02:18:12 +02:00
codl 52781672e0
Merge branch 'master' into archive-warn 2018-10-06 02:15:57 +02:00
codl bbcbbffeaa
update requirements manifests 2018-10-06 02:09:48 +02:00
codl fa6a462755
bump pytest and pillow versions 2018-10-06 02:05:58 +02:00
codl 8d71c868f6
travis npm version is too old for npm ci 2018-10-06 02:01:35 +02:00
codl 12a09eddab
remove yarn lock. just use npm 2018-10-06 01:54:36 +02:00
dependabot[bot] eb30308c94 Bump rollup-plugin-svelte from 4.3.1 to 4.3.2
Bumps [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/rollup/rollup-plugin-svelte/releases)
- [Changelog](https://github.com/rollup/rollup-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-svelte/compare/v4.3.1...v4.3.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-05 23:53:28 +00:00
dependabot[bot] f9c85093c0 Bump rollup from 0.66.2 to 0.66.4
Bumps [rollup](https://github.com/rollup/rollup) from 0.66.2 to 0.66.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v0.66.2...v0.66.4)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-05 23:49:41 +00:00
dependabot[bot] e17736e117 Bump brotli from 1.0.4 to 1.0.6
Bumps [brotli](https://github.com/google/brotli) from 1.0.4 to 1.0.6.
- [Release notes](https://github.com/google/brotli/releases)
- [Commits](https://github.com/google/brotli/compare/v1.0.4...v1.0.6)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-10-05 23:45:29 +00:00
codl efd0fe60f8
update changelog 2018-10-06 01:43:45 +02:00
codl 93299bd72f
polish archive size warning
copy, styling

lowered the threshold to 10 MB and made it not disable the form in case
someone does have a giant tweet archive
2018-10-06 01:33:35 +02:00
codl c89602315f
remove leftover console.logs 2018-10-06 01:30:01 +02:00
codl cd096599a2
add warning when trying to upload a large twitter archive 2018-10-06 00:57:25 +02:00
codl 5f2922f27a
doit: js depends on rollup config 2018-10-06 00:55:11 +02:00
dependabot[bot] 297d9398bb Bump rollup-plugin-svelte from 4.3.0 to 4.3.1
Bumps [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) from 4.3.0 to 4.3.1.
- [Release notes](https://github.com/rollup/rollup-plugin-svelte/releases)
- [Changelog](https://github.com/rollup/rollup-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-svelte/compare/v4.3.0...v4.3.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-24 10:49:23 +00:00
codl 592ac58333
Merge pull request #83 from codl/dependabot/pip/pytest-3.8.1
Bump pytest from 3.8.0 to 3.8.1
2018-09-24 12:34:27 +02:00
dependabot[bot] 293ffa0d4a
Bump pytest from 3.8.0 to 3.8.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 3.8.0 to 3.8.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/3.8.0...3.8.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-24 08:03:55 +00:00
codl 307bbceb80
Merge pull request #82 from codl/dependabot/npm_and_yarn/rollup-0.66.2
Bump rollup from 0.66.1 to 0.66.2
2018-09-21 23:08:41 +02:00
dependabot[bot] 05e2a50466
Bump rollup from 0.66.1 to 0.66.2
Bumps [rollup](https://github.com/rollup/rollup) from 0.66.1 to 0.66.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v0.66.1...v0.66.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-21 06:30:54 +00:00
codl e171c03220
Merge pull request #79 from codl/dependabot/npm_and_yarn/rollup-0.66.1
Bump rollup from 0.66.0 to 0.66.1
2018-09-20 10:32:29 +02:00
dependabot[bot] 35fb8fe6f6
Bump rollup from 0.66.0 to 0.66.1
Bumps [rollup](https://github.com/rollup/rollup) from 0.66.0 to 0.66.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v0.66.0...v0.66.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-20 08:29:50 +00:00
codl b9a60d6fee
Merge pull request #80 from codl/dependabot/npm_and_yarn/svelte-2.13.5
Bump svelte from 2.13.4 to 2.13.5
2018-09-20 10:27:35 +02:00
dependabot[bot] 7f945e54ee
Bump svelte from 2.13.4 to 2.13.5
Bumps [svelte](https://github.com/sveltejs/svelte) from 2.13.4 to 2.13.5.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v2.13.4...v2.13.5)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-20 08:23:57 +00:00
codl 1639d4cf16
Merge pull request #81 from codl/dependabot/pip/sqlalchemy-1.2.12
Bump sqlalchemy from 1.2.11 to 1.2.12
2018-09-20 10:22:06 +02:00
dependabot[bot] 5723c2bea5
Bump sqlalchemy from 1.2.11 to 1.2.12
Bumps [sqlalchemy](https://bitbucket.org/zzzeek/sqlalchemy) from 1.2.11 to 1.2.12.
- [Changelog](https://bitbucket.org/zzzeek/sqlalchemy/src/master/CHANGES)
- [Commits](https://bitbucket.org/zzzeek/sqlalchemy/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-20 06:57:11 +00:00
dependabot[bot] 510c3689fa Bump rollup from 0.65.2 to 0.66.0
Bumps [rollup](https://github.com/rollup/rollup) from 0.65.2 to 0.66.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v0.65.2...v0.66.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-17 20:39:27 +02:00
dependabot[bot] a8afa8a38a Bump svelte from 2.13.3 to 2.13.4
Bumps [svelte](https://github.com/sveltejs/svelte) from 2.13.3 to 2.13.4.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v2.13.3...v2.13.4)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-07 10:37:58 +00:00
dependabot[bot] 61c71800d9 Bump pytest-cov from 2.5.1 to 2.6.0
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.5.1 to 2.6.0.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-06 19:50:41 +00:00
dependabot[bot] 35e0bceb6f Bump pytest from 3.7.3 to 3.8.0 (#75)
* Bump pytest from 3.7.3 to 3.8.0

Bumps [pytest](https://github.com/pytest-dev/pytest) from 3.7.3 to 3.8.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/3.7.3...3.8.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>

* lock requirements.txt
2018-09-06 21:40:47 +02:00
dependabot[bot] 16c06b3f42 Bump rollup from 0.65.0 to 0.65.2
Bumps [rollup](https://github.com/rollup/rollup) from 0.65.0 to 0.65.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v0.65.0...v0.65.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-06 19:05:50 +00:00
dependabot[bot] 05dfe8d998 Bump svelte from 2.13.2 to 2.13.3
Bumps [svelte](https://github.com/sveltejs/svelte) from 2.13.2 to 2.13.3.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v2.13.2...v2.13.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-06 19:00:39 +00:00
dependabot[bot] 77cb793339 Bump rollup-plugin-svelte from 4.2.1 to 4.3.0 (#71)
Bumps [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) from 4.2.1 to 4.3.0.
- [Release notes](https://github.com/rollup/rollup-plugin-svelte/releases)
- [Changelog](https://github.com/rollup/rollup-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-svelte/compare/v4.2.1...v4.3.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-09-04 10:17:34 +02:00
dependabot[bot] 0257a7694d Bump pytest from 3.7.2 to 3.7.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 3.7.2 to 3.7.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/3.7.2...3.7.3)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-29 10:10:19 +00:00
dependabot[bot] 3cc348e819 Bump svelte from 2.11.0 to 2.13.2
Bumps [svelte](https://github.com/sveltejs/svelte) from 2.11.0 to 2.13.2.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v2.11.0...v2.13.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-29 10:03:29 +00:00
dependabot[bot] 6208dd3f9a Bump rollup from 0.64.1 to 0.65.0 (#66)
Bumps [rollup](https://github.com/rollup/rollup) from 0.64.1 to 0.65.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-29 11:59:21 +02:00
dependabot[bot] 6eb7b770ba Bump sqlalchemy from 1.2.10 to 1.2.11 (#63)
Bumps [sqlalchemy](https://bitbucket.org/zzzeek/sqlalchemy) from 1.2.10 to 1.2.11.
- [Changelog](https://bitbucket.org/zzzeek/sqlalchemy/src/master/CHANGES)
- [Commits](https://bitbucket.org/zzzeek/sqlalchemy/commits)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-21 12:35:54 +02:00
dependabot[bot] 42f69080b7 Bump pytest from 3.7.1 to 3.7.2 (#62)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 3.7.1 to 3.7.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/3.7.1...3.7.2)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-20 14:30:00 +02:00
dependabot[bot] b1cde1f161 Bump svelte from 2.10.1 to 2.11.0 (#61)
Bumps [svelte](https://github.com/sveltejs/svelte) from 2.10.1 to 2.11.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v2.10.1...v2.11.0)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-15 11:07:31 +02:00
codl c54390801c
code quality fixes
also ran yapf on tasks.py
2018-08-14 03:51:14 +02:00
dependabot[bot] 49d87fd6d4 Bump rollup-plugin-svelte from 4.1.0 to 4.2.1
Bumps [rollup-plugin-svelte](https://github.com/rollup/rollup-plugin-svelte) from 4.1.0 to 4.2.1.
- [Release notes](https://github.com/rollup/rollup-plugin-svelte/releases)
- [Changelog](https://github.com/rollup/rollup-plugin-svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup-plugin-svelte/compare/v4.1.0...v4.2.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-13 17:15:58 +00:00
dependabot[bot] e012788cfb Bump rollup from 0.58.1 to 0.64.1 (#56)
Bumps [rollup](https://github.com/rollup/rollup) from 0.58.1 to 0.64.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v0.58.1...v0.64.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-13 18:37:15 +02:00
dependabot[bot] efe4c02911 Bump svelte from 2.0.0 to 2.10.1 (#58)
Bumps [svelte](https://github.com/sveltejs/svelte) from 2.0.0 to 2.10.1.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/compare/v2.0.0...v2.10.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-13 18:28:22 +02:00
codl db22435df2
make travis run npm install 2018-08-13 18:11:22 +02:00
codl 7238082f23
test doit: need to also mock sys.argv :/ :/ :/ :/ 2018-08-13 18:03:58 +02:00
codl 8f013a7bc7
add test to check that doit runs successfully 2018-08-13 14:41:28 +02:00
codl 9713e72bc6
travis: update for python 3.7 (#50)
* travis: update for python 3.7
* pipenv update. celery didnt got updated for 3.7 in time
2018-08-11 23:28:25 +02:00
codl fb1dc361bf
Revert "Merge pull request #51 from codl/dependabot/pip/pytest-3.7.1"
This reverts commit 7149ef7910, reversing
changes made to 9b2394f6e7.
2018-08-07 21:01:17 +02:00
dependabot[bot] 7149ef7910
Merge pull request #51 from codl/dependabot/pip/pytest-3.7.1 2018-08-07 13:31:01 +00:00
dependabot[bot] 2ea122900e
Bump pytest from 3.5.1 to 3.7.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 3.5.1 to 3.7.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/3.5.1...3.7.1)

Signed-off-by: dependabot[bot] <support@dependabot.com>
2018-08-07 13:19:10 +00:00
codl 9b2394f6e7
remove social media from contact options 2018-08-07 14:54:57 +02:00
codl b8027182df
release v1.6.0 2018-07-06 09:38:52 +02:00
codl e3a8810450
update changelog 2018-07-06 01:48:33 +02:00
codl b3de8e10ac
add exponential backoff 2018-07-06 01:44:30 +02:00
codl da22593057
update probot stale settings (#44) 2018-06-08 00:47:59 +02:00
codl 78ca3dc0ea
release v1.2.1 2018-05-08 00:52:18 +02:00
codl 95d7d94c25
raise number of known instances slots to 5
also limit number of log-in buttons to 5 on the front page
2018-05-08 00:49:23 +02:00
codl f3bc2e53c7
v1.2.0 2018-05-08 00:26:09 +02:00
codl da8c3d90c8
clarity 2018-05-08 00:17:56 +02:00
codl cacd271683
clarity 2018-05-08 00:14:45 +02:00
codl 5cb84e1fbd
lower mastodon.social default hits in known instances to 0
if the user uses mastodon.social, it will be raised to 1, if not then it
can be replaced by the third instance the user logs into
2018-05-08 00:09:48 +02:00
codl 4c7a919079
show user's top known instances on the login page 2018-05-08 00:06:36 +02:00
codl 447923b1f1
add mechanism for keeeping track of a user's instances 2018-05-07 23:50:37 +02:00
codl 0c99c13ef5
add .pytest_cache to gitignore 2018-05-07 23:50:09 +02:00
codl c42b0f7f87
move popular instances query to libforget.mastodon 2018-05-07 23:49:28 +02:00
codl 0caac7c679
Update requirements (#35)
* update requirements, lock versions for flask and mastodon.py

* use gunicorn as dev server

* travis: install dev requirements
2018-05-07 21:37:27 +02:00
codl e2976e58b7
v1.1.3 2018-04-25 15:14:24 +02:00
codl 022cf7520a
make radiostrips accessible 2018-04-25 15:04:08 +02:00
codl 07ad982b96
prevent invisible radio buttons from escaping the radiostrip 2018-04-25 14:42:31 +02:00
codl c219a8b532
unify the look of radiostrips and the other buttons 2018-04-25 14:01:32 +02:00
codl 2f425baefe
remove libraries.io shield. it's almost always broken 2018-04-25 09:17:59 +02:00
codl 76bfb46207
README formatting. break up huge paragraphs 2018-04-25 09:17:59 +02:00
codl 4c4ab0159b
replace twitter contact link with mastodon in README 2018-04-25 09:17:59 +02:00
codl 3dcbf419bc set up .codecov.yml to ignore version.py and also shut up in PRs 2018-04-25 09:05:08 +02:00
codl 1e5545f105 v1.1.2 2018-04-25 08:49:04 +02:00
codl aba1635716 fix #25 error when submitting settings with JS disabled 2018-04-25 08:29:15 +02:00
codl 2d56d865d6
version bump 2018-04-19 16:57:29 +02:00
codl de43749411
add versioneer to dev-dependencies 2018-04-19 16:54:43 +02:00
codl df316cc65e
update python deps. also update package-lock 2018-04-19 16:50:14 +02:00
codl 122fe86315
update svelte 2018-04-19 16:18:31 +02:00
codl 86686938a4
update changelog 2018-02-03 17:55:23 +01:00
codl b54075c485
fix post-receive hook again. dumbass 2018-02-03 17:47:44 +01:00
codl c716dcb35d
actually just use a clone instead 2018-02-03 17:11:06 +01:00
codl 2cc98cb11b
update post-receive hook to use git archive 2018-02-03 17:10:52 +01:00
codl e36fa1e999
version bump 2018-01-31 23:27:23 +01:00
codl 520eddba5b
🆙 update deps 2018-01-31 23:16:55 +01:00
codl e2f1ca1732
replace messy version stuff with versioneer 2018-01-31 23:16:54 +01:00
codl 35560e17b6
leave me alone travis thats not even my fault thats celerys fault 2018-01-30 21:15:35 +01:00
codl b95b47018a
set celery to autoscale from 8 to 64 processes 2018-01-19 03:30:38 +01:00
codl 46cef0852b
frick you travis 2018-01-03 18:33:56 +01:00
codl 51e070af3d
three-way favourite policy. closes #18 2018-01-03 18:00:59 +01:00
codl 5c47db8cc4
hotfix: sqlalchemy 1.2 no longer coerces strings to booleans 2018-01-03 17:58:13 +01:00
codl 4bdaa5a8fa
fuck you pip 2018-01-03 17:30:25 +01:00
codl 3e725df58b
update maintained badge 2018-01-03 12:55:01 +01:00
codl ec733c30d6
fix pipfile, requirements.txt 2018-01-03 12:55:01 +01:00
codl 9696b6e448
fix revoked tokens not being found because of localised error messages 2018-01-03 12:24:35 +01:00
codl b11ffbbc11
more knob twiddling 2017-12-28 15:30:02 +01:00
codl e6a582431d
twiddle knob 2017-12-28 13:22:15 +01:00
codl 8b7db5cce2
simplify mastodon instance scoring 2017-12-28 13:12:55 +01:00
codl 9574476491
OOPS WHOOPS uh oh 2017-12-28 03:25:27 +01:00
codl ada719faeb
add ability to keep only posts without media
closes #16
2017-12-28 03:02:08 +01:00
codl 40960fc7fd
take direct messages into account in estimate 2017-12-27 22:16:13 +01:00
codl c0c1591dfa
shorten delay between delete queue ups 2017-12-27 22:01:45 +01:00
codl 8d8a21c6a2
take reblogs into account in estimated post count 2017-12-27 22:00:58 +01:00
codl f2cdc30c9e
fix #17 ignore fav on retweets
im not sure how it works on mastodon so i just took the safe option
2017-12-27 21:25:09 +01:00
codl 3c6a9e4158
track which posts are reblogs 2017-12-27 21:23:13 +01:00
codl b72fbac24d
no! i forgot to also store fav/rt counts on twitter 2017-12-27 21:03:12 +01:00
codl 253bd78b12
duh 2017-12-27 20:51:21 +01:00
codl 132007f91f
store fav/reblog count. first step towards #7 2017-12-27 20:49:19 +01:00
codl 79cd7127c5
add config file for probot's stale 2017-12-27 20:20:31 +01:00
codl add2c3883d
🆙 update requirements 2017-12-21 16:16:13 +01:00
codl b057002ccb
add cache headers to sentry js
that oughta shut webpagetest up >:(
2017-12-19 16:30:54 +01:00
codl 89382d70e2
replace plain radio buttons with radio strips 2017-12-14 16:59:33 +01:00
codl 3b2cd362d9
forgot to also change the link oops 2017-11-25 00:54:56 +01:00
codl 0f4a9fcec7
replace coveralls badge with codecov badge 2017-11-25 00:50:20 +01:00
codl 84c541279e
coveralls -> codecov 2017-11-25 00:43:01 +01:00
codl f7bc5e9aa0
🆙 update requirements 2017-11-19 01:03:34 +01:00
codl e48786290a
fix real old typo 2017-11-19 01:00:30 +01:00
codl 053208eca1
shoutouts to the flask-migrate maintainer for adding a wheel
no shoutouts to pip for not trying the source distribution when the
wheel doesn't match the hash
2017-11-18 10:53:13 +01:00
codl c01bdd2fd3
you know what, fuck it. this is v1.0.0 2017-11-13 01:15:03 +01:00
codl 11b78e887f
update changelog 2017-11-13 01:13:43 +01:00
codl fe52c4424f
un-kaching 💸 remove codesponsor from readme 2017-11-13 01:00:33 +01:00
codl 59e5ab2390
fix #14 Error returning to forget after cancelling authorization
turns out request.args has some special behaviour that forces a 400
instead of throwing a KeyError when trying to read an arg that wasn't
supplied :/ havent seen anything about it in flask's docs
2017-11-02 15:47:04 +01:00
codl 75e5f52871
make sure to convert mastodon post IDs to integers
mastodon v2.0.0 changed IDs to be snowflake-like and returns them as
strings in the API
2017-10-18 21:06:28 +02:00
codl a057669a21
🆙 update requirements 2017-10-18 21:06:28 +02:00
codl 81f3d5dff5
front page mastodon buttons: sort mastodon.social first 2017-10-15 20:47:58 +02:00
codl 3d8c809e64
add health check endpoint 2017-10-15 20:38:39 +02:00
codl 4a2450e28f
kaching 💶💶 add codesponsor to readme 2017-10-08 08:01:14 +02:00
codl 8422b6d89b
add soft and hard time limits to every task 2017-09-30 20:44:27 +02:00
codl a6870d3775
codacy 2017-09-24 23:54:03 +02:00
codl 1b07bf1020
codacy 2017-09-24 23:49:08 +02:00
codl 4f1fb8f262
step into the future with pipfiles 2017-09-24 23:22:19 +02:00
codl 366030dbda
remove flask-limiter 2017-09-24 23:22:19 +02:00
codl 6fd1d6979b
remove unused iso8601 req 2017-09-24 23:22:19 +02:00
codl 53dd39ea0d
travis: add python 3.6-dev target 2017-09-24 21:54:22 +02:00
codl b69f5db0dd
cleaner redis isolation 2017-09-24 16:43:54 +02:00
codl 520560ce79
remove flask-limiter, make sure redis isnt initialised early 2017-09-24 16:37:38 +02:00
codl fe1db3cf36
test version 👀 2017-09-24 13:11:06 +02:00
codl 4f3c877d0c
lower timeout and increase payload size on brotli timeout test
it keeps failing because travis' machines are too fast lol
2017-09-24 12:56:23 +02:00
codl 79e06784af
increase default timeout in brotli 2017-09-23 19:38:01 +02:00
codl 8911c57ee4
double the default timeout on img proxy 2017-09-23 10:34:01 +02:00
codl 66d87fda58
oops 2017-09-23 10:33:46 +02:00
codl 526efbeded
hint that you have to upload the zip file from twitter 2017-09-21 21:18:01 +02:00
codl 256213175f
ah! this snuck into my commit 2017-09-21 13:56:16 +02:00
codl 7967cc686c
maybe if i cache mastodon api instances i can avoid 50% of calls?
im just throwing shit at the wall here im running out of ideas

this could also fuck everything up idk let's see

uhh issue #10
2017-09-21 13:54:41 +02:00
codl a649d21549
brotli: set header to timeout if timeout was immmediate (0) 2017-09-21 09:51:59 +02:00
codl 0e185c67a5
ahhhhh 2017-09-21 07:26:01 +02:00
codl eb6fcd700a
Revert "make sure post id is an actual integer"
This reverts commit e9653f34be.
2017-09-21 07:24:18 +02:00
codl 1123d44b30
Revert "protect against invalid posts"
This reverts commit 4e5c2b1f9a.
2017-09-21 07:19:35 +02:00
codl e9653f34be
make sure post id is an actual integer 2017-09-21 07:19:13 +02:00
codl 4e5c2b1f9a
protect against invalid posts
(???) i dont quite understand why i have to do this but some server is
throwing posts at me without an id or a status or much of anything so..
2017-09-21 06:50:01 +02:00
codl fd0bd49bdd
lol oops 2017-09-21 02:33:00 +02:00
codl fb65bcf0ac
dont open one redis client per task 👀 2017-09-21 02:27:30 +02:00
codl 0d7c1af13d
twiddle knob 2017-09-21 02:19:42 +02:00
codl 0ad980f499
twiddle knob 2017-09-21 01:25:25 +02:00
codl 0c3f603db5
whoops forgot to git add privacy.html 2017-09-20 23:29:54 +02:00
codl 5ba5f2b460
split privacy policy from about page 2017-09-20 23:28:12 +02:00
codl c135dc793e
rename lib to libforget 2017-09-20 23:04:44 +02:00
codl 89e35786d0
oops!!!!! 2017-09-20 14:44:01 +02:00
codl 06c0de29da
oops 2017-09-20 14:42:45 +02:00
codl 6bce7b1d04
make sure only one of each task runs at once 2017-09-20 14:39:31 +02:00
codl b9b3d8bab2
fix procfile to allow a decent amout of concurrent jobs 2017-09-20 13:45:20 +02:00
codl b0b17ae39f
oops 2017-09-20 12:58:16 +02:00
codl 53c2e8a023
another measure to avoid hitting rate limits 2017-09-20 12:41:10 +02:00
codl 33cc82b1ca
mastodon avatars are not circles (sometimes) 2017-09-19 02:44:08 +02:00
codl ab085aabf8
oh my god are you serious with this 2017-09-19 01:43:50 +02:00
codl d224edcc58
imgproxy: store more headers, use cache-control to determine ttl 2017-09-18 11:23:59 +02:00
codl d6fe2ff9b6
imgproxy: use global redis uri 2017-09-18 10:29:52 +02:00
codl 2fcbddd843
I SPELLED BROTLI WRONG 2017-09-17 19:25:10 +02:00
codl d77c9a1149
v0.0.10
bout damn time i bumped the version number
2017-09-17 19:17:53 +02:00
codl 02b564283f
twiddle the knob 2017-09-17 19:11:45 +02:00
codl 34b6f622a1
oops! oops. forgot to commit transaction 2017-09-17 15:21:48 +02:00
codl 614db2964d
update mastodon instance score based on active accounts
this is far more interesting than updating every time someone logs in,
since users don't have a reason to log in very often
2017-09-17 15:12:46 +02:00
codl 4b1a6a3a90
fix error detection in mastodon.py 2017-09-17 15:12:01 +02:00
codl 260f15d44a
fix: two login buttons when there are no mastodon instances 2017-09-17 15:11:14 +02:00
codl fbecb2f9fc
styles: replace secondary btn border with a box-shadow for alignment 2017-09-17 14:26:53 +02:00
codl 8a60b85586
(laughing) more buttons! 2017-09-17 13:35:26 +02:00
codl a706f6fd6e
WHOOPS whoops whoops whoops whoops 2017-09-17 13:21:35 +02:00
codl a656463d0a
unify button-buttons and link-buttons 2017-09-17 13:20:30 +02:00
codl 53ba4ff294
split about and logged in pages into two different paths 2017-09-17 12:29:49 +02:00
codl cce01b3b51
fix: mastodon.py now throws on revoked tokens 2017-09-17 11:07:25 +02:00
codl a787f26e9a
give avatar an empty alt attribute 2017-09-16 20:52:21 +02:00
codl 8246e276f4
mastodon's 'post not found' message changed at some point apparently? idk fuck 2017-09-16 20:24:14 +02:00
codl 7c8c49a2ae
an attempt at preventing hitting rate limits on mastodon
most tasks don't need to be retried if they have failed, they will be
queued up again in the next batch
2017-09-16 20:08:06 +02:00
codl 8d82780e32
make rate limits more lenient on user-facing pages 2017-09-16 20:04:47 +02:00
codl 0b06b5f12f
whoops i ended up reimplementing *all* of github camo 2017-09-16 18:25:20 +02:00
codl 0ecc9c25ca
whoops! fix imgproxy expiry time scale 2017-09-16 14:11:17 +02:00
codl 5e1ce21c82
proxy avatars
this fixes some potential issues when connecting to a non-https mastodon
server

you shouldn't connect to a non-https mastodon server in general but you
know, whatever
2017-09-16 13:58:02 +02:00
codl e4b443ce45
split off routes.py
also add robots.txt and humans.txt routes
2017-09-16 12:22:17 +02:00
codl 583dedf633
fix settings.js for microsoft edge 2017-09-16 09:28:39 +02:00
codl cd30b6b606
fix sentry not showing account info correctly (i hope?) 2017-09-15 08:51:14 +02:00
codl 1ff90e499f
travis: don't include coveralls in the tests 2017-09-14 10:36:12 +02:00
codl 551c8d30e2
🆙 update mastodon.py to 1.1.1 2017-09-10 23:55:43 +02:00
codl a387cbeeb1
readme: add note about tests 2017-09-10 15:11:27 +02:00
codl 47c41bad00
uh hehe i forgot to actually remove the line instead of just commenting it 2017-09-10 14:26:17 +02:00
codl 7b4c8a8fe1
custom 500: only catch 500 not every exception
if we catch Exception then flask's error handler never runs and neither
does raven's
2017-09-10 14:24:30 +02:00
codl 7599565344
add proper error pages for 404, 500 2017-09-10 13:20:53 +02:00
codl fcabf262c5
🆙 update mastodon.py 2017-09-10 12:33:02 +02:00
codl 599f0ba2fb
Revert "🆙 mastodon.py 1.1.0"
This reverts commit 65fc959d96.
2017-09-10 11:40:48 +02:00
codl 27c0369b94
i didn't mean to include that in the previous commit! 2017-09-09 19:42:47 +02:00
codl f7ea8e18bf
travis: cache pip cache 2017-09-09 10:28:12 +02:00
codl 2f426fe5f7
fix: no need to parse date ourself, mastodon.py v1.1.0 does it 2017-09-09 00:21:46 +02:00
codl 5a81c2bf14
fix unreliable timeout test 2017-09-09 00:06:09 +02:00
codl 6a69fa8ed4
add travis & coveralls badges to readme 2017-09-08 23:38:55 +02:00
codl 1d72bbc716
i'm big dummy!!!! 2017-09-08 23:36:29 +02:00
codl 2eea031a9c
update requirements for tests, add travis.yml 2017-09-08 23:34:12 +02:00
codl dff825db06
test libbrotli 2017-09-08 23:19:59 +02:00
codl 65fc959d96
🆙 mastodon.py 1.1.0 2017-09-08 21:19:28 +02:00
codl 43903a5776
mastodon login: also strip trailing path 2017-09-08 00:43:19 +02:00
codl d7e1a7f179
mastodon login: strip protocol 2017-09-08 00:43:19 +02:00
codl 146dd263c9
unify redis config (closes #8)
also, not sure why brotli was being initialised in routes not in app
2017-09-07 01:10:02 +02:00
codl fda4428572
remove default secret key as it is no longer used
(lol hunter2)
2017-09-07 00:46:57 +02:00
codl 208fee88ec
remove debug feature to log all sql queries 2017-09-07 00:45:19 +02:00
codl b36a375a72
add maintained status badge 2017-09-07 00:34:54 +02:00
codl 0d1b09b80b
replace native codacy badge with a shields.io badge for consistency 2017-09-06 16:24:59 +02:00
codl 83f2d12d20
oops 2017-09-06 16:23:00 +02:00
codl d90779aad5
add user count badge to readme 2017-09-06 16:21:33 +02:00
codl 2190d5dd86
add user count badge 2017-09-06 16:19:13 +02:00
codl 8297a88c51
add libraries.io badge 2017-09-06 16:00:17 +02:00
codl 06990aa43c
🆙 upgrade requirements 2017-09-06 13:41:49 +02:00
codl 68d086d64e
pylint 2017-09-06 13:10:08 +02:00
codl bde08c6c49
pylint 2017-09-06 13:08:06 +02:00
codl ee72c50ab9
eslint 2017-09-06 13:06:59 +02:00
codl 840f56a769
send PermanentErrors to sentry 2017-09-05 13:01:33 +02:00
codl 4b44682827
catch more twitter exceptions 2017-09-05 12:58:57 +02:00
codl 2ad076e63c
oops. catch urlerror when doing verify_credentials 2017-09-05 00:08:33 +02:00
codl e8dbcb1d14
oops!!!!! 2017-09-05 00:08:33 +02:00
codl 214e1f30cd
oops handle twitter errors correctly 2017-09-04 23:08:19 +02:00
codl edf7732e67
overhaul and abstract errors in service libs
also add support for making an account dormant if there is a permanent
error
2017-09-04 22:15:05 +02:00
codl 5e22a0531f
capitalise service names 2017-09-04 22:15:04 +02:00
codl d851f562e4
consolidate lib.session and lib.auth into lib.auth 2017-09-04 22:15:04 +02:00
codl 93ab3294ce
oops 2017-09-03 01:23:54 +02:00
codl ae093c504c
add an explicit dismiss button to reason banner 2017-09-03 00:51:54 +02:00
codl 23794acebe
fix: mastodon.py didnt commit after deleting an invalid token 2017-09-02 20:30:37 +02:00
codl 7e677f4f97
whoops. move actions taken on unreachable accs to the celery task
it's not each service lib's job to deal with this
2017-09-02 20:00:44 +02:00
codl 73b7ee2a53
settings.js: remove reason banner when enabling 2017-09-02 19:51:59 +02:00
codl 1d677de7b4
add mechanism for showing reason for disabling account 2017-09-02 19:47:18 +02:00
codl feef504fa0
settings.js: when logged out, redirect to index 2017-09-02 19:05:57 +02:00
codl 109cbf31d9
aughh sentryyyyy 2017-09-02 19:05:46 +02:00
codl ec75fbaf10
oops
the sentry flask middleware has *some* of raven's API but not all of
it???? why
2017-09-02 10:56:28 +02:00
codl dda8537b53
model: __str__ not __repr__ 2017-09-02 10:44:21 +02:00
codl 9bc92fc1f0
report revoked or otherwise erroneous creds to sentry 2017-09-02 10:43:52 +02:00
codl 2c02dd63bc
more fiddling with timing of javascript load 2017-09-01 02:35:33 +02:00
codl e02b761b53
fix race condition when settings.js loads faster than the dom is parsed 2017-09-01 02:15:01 +02:00
codl 8506c920b6
fiddle with schedules 2017-09-01 01:45:24 +02:00
codl 8a8a7e593c
fix not being able to update delete_every when next_delete is null 2017-09-01 01:34:12 +02:00
codl 6056d1c28f
settings.js: increase initial update interval to 1.5s 2017-09-01 01:07:08 +02:00
codl 2a6d0b612f
update front page copy 2017-09-01 00:43:19 +02:00
codl cfe29a7e25
update footer 2017-09-01 00:09:05 +02:00
codl 6f48a28387
update readme 2017-08-31 23:51:59 +02:00
codl 69b7d8baf7
v0.0.9 2017-08-31 23:19:54 +02:00
codl 14e260a56e
update git hook with yarn 2017-08-31 22:43:16 +02:00
codl 705084a0b0
remove /api/viewer/timers
it never saw use
2017-08-31 22:43:16 +02:00
codl 4b28669327
small visual changes 2017-08-31 21:56:17 +02:00
codl 96e33f7179
fix display when last_delete is null 2017-08-31 21:55:26 +02:00
codl 8ab74eb306
default next_delete to null 2017-08-31 21:22:22 +02:00
codl c897edf294
whoops!!!! whoops 2017-08-31 21:16:38 +02:00
codl 278c5b9e2b
fiddle with the fuzzy time some more 2017-08-31 21:13:41 +02:00
codl 3d303a3cf5
update readme with yarn 2017-08-31 21:09:52 +02:00
codl 2ad5df5fbb
lower minimum staleness for fetch 2017-08-31 20:48:26 +02:00
codl c2013197f9
settings.js: update viewer immediately after enabling/disabling 2017-08-31 20:48:00 +02:00
codl cd13094bad
make sure to reset delete timer when enabling 2017-08-31 20:47:29 +02:00
codl e6ddf2f120
add sentry js back in 2017-08-31 20:46:38 +02:00
codl 6696188601
Revert "you know what, screw sentry. bye"
This reverts commit 175e313e03.
2017-08-31 19:58:48 +02:00
codl 66895e7108
tiny style fix 2017-08-31 19:54:36 +02:00
codl 04654a637c
(rips shirt) HAUUUGH JARVASCRIPT 2017-08-31 18:59:09 +02:00
codl 984fce51d2
fix broke ass settings 2017-08-30 00:15:00 +02:00
codl 20d765e0d1
pylint 2017-08-29 21:31:30 +02:00
The Codacy Badger 8f40dbe490 Add Codacy badge 2017-08-29 20:50:50 +02:00
codl 571ac0633b
fix 'cannot choose from an empty sequence' 2017-08-29 20:48:04 +02:00
codl 06bf189a33
an attempt at fixing the weird deadlock issue ive been having 2017-08-29 20:45:42 +02:00
codl 6495135124
eslint 2017-08-29 18:38:54 +02:00
codl 9a86b45268
fix twitter when all refreshed posts are gone 2017-08-29 17:50:19 +02:00
codl 20ed4175e4
limit to one tweet per delete round
deleting hundreds of tweets every 40 seconds is making us hit the rate
limit, dont do that
2017-08-29 17:44:50 +02:00
codl 1502f783db
add last_delete back into account 2017-08-29 17:23:28 +02:00
codl 4da2412421
oops!!!!!!!!!!!!!!!!!!!!!!!!! 💦 2017-08-29 17:06:44 +02:00
codl 8e0b4784af
update settings.js for screen name as title text 2017-08-29 16:59:52 +02:00
codl 32fa0b055d
add screen name as title text on display name 2017-08-29 16:57:49 +02:00
codl 5b01c53aac
add mastodon instance to screen_name 2017-08-29 16:57:30 +02:00
codl 2e0b598658
whoops oopsie oh bother 2017-08-29 16:45:00 +02:00
codl d4d30d0530
nicer error message on mastodon app creation failure
also log more exceptions to sentry
2017-08-29 16:41:11 +02:00
codl e07e052d7d
oops 2017-08-29 16:31:43 +02:00
codl 1a2fda9c86
update readme 😛 2017-08-29 16:11:58 +02:00
codl b231ea7c00
remove unnecessary lambdas 2017-08-29 15:17:47 +02:00
codl f9a6bfe260
rename scales to timescales.py 2017-08-29 15:00:08 +02:00
codl 007aec7529
flakes8 2017-08-29 14:46:32 +02:00
codl 2c4d6b9f63
fix issues raised by bandit 2017-08-29 13:26:32 +02:00
codl 78013ed1e9
twiddle knob
i set the timeout based on a timeit on my desktop instead of my server
like an Absolute Fool
2017-08-29 10:56:21 +02:00
codl e4dbdf98ee
reduce ttl and timeout on brotli generation
recent changes (csrf tokens) have made brotli caches much less durable
since each session for a same user gets a different page
2017-08-29 09:35:16 +02:00
codl 88b559a2f1
ensure proper mimetype when serving webp images 2017-08-29 01:29:55 +02:00
codl c328332ff9
settings.js fix: keep polling viewer even if last request failed 2017-08-29 01:17:47 +02:00
codl d2c3f7025c
Revert "add shoddy statsd support"
This reverts commit 8c0c521f6f.

yea this is useless and a waste of time
2017-08-28 23:25:26 +02:00
codl 81409673f3
settings.js: adaptive interval for updates 2017-08-28 17:53:13 +02:00
codl 19982af3ee
hgghn celery statsd bad 2017-08-28 17:24:02 +02:00
codl 2823f5fb76
celery statsd 2017-08-28 17:19:07 +02:00
codl 8c0c521f6f
add shoddy statsd support 2017-08-28 17:13:12 +02:00
codl 719c54b046
mastodon instances: lower max popularity to 40
100 is really high considering forget's popularity
2017-08-28 15:37:49 +02:00
codl 31bdf87c89
csp: only set upgrade-insecure-requests when over https 2017-08-28 09:12:32 +02:00
codl 3be1b79b92
mozilla observatory retire bitch 2017-08-28 01:58:00 +02:00
codl 175e313e03
you know what, screw sentry. bye 2017-08-28 01:53:59 +02:00
codl af407ff1f2
fix hsts header 2017-08-28 01:52:22 +02:00
codl 16f6739189
fix csp for sentry 2017-08-28 01:50:16 +02:00
codl e8f45c1af6
add security headers 2017-08-28 01:47:01 +02:00
codl 8c1de93eb3
v0.0.8 2017-08-27 12:40:13 +02:00
codl cdc30b4f8b
add csrf token to warning pages (whoops! closes #5) 2017-08-26 15:55:48 +02:00
codl ccf1ca9c56
add csrf tokens 🔒🔒🔒🔒 2017-08-25 10:50:11 +02:00
codl 75907c8568
aha eheh whoops uh oh hhHEHhaha oops 2017-08-24 18:53:24 +02:00
codl e99a045c41
following the last commit, it's prudent to invalidate all static urls
ive been meaning to change that url structure anyway because it's real
goofy to have these 'static-483914848324' directories
2017-08-24 18:49:37 +02:00
codl 6d8991ee65
doit overhaul
most notably now we preserve the mtime of the source files so
cachebusted static urls won't change every time everything is
regenerated for whatever reason
2017-08-24 18:43:28 +02:00
codl 092cc1919d
resize twitter and mastodon logo 2017-08-24 15:40:03 +02:00
codl effe4d1381
mastodon instances: make order deterministic 2017-08-24 14:49:20 +02:00
codl ab0aa3483a
fetch account on mastodon log in 2017-08-24 14:48:08 +02:00
codl 35a1391e5f
twiddle knob 2017-08-24 00:30:11 +02:00
codl ea27e26932
remove "a codl joint" from footer. its kinda awkward 2017-08-24 00:01:00 +02:00
codl 5b047e4b94
nicer, more compact mastodon login buttons 2017-08-23 23:32:21 +02:00
codl fac2a0e9b3
oops??? why? 2017-08-23 23:18:56 +02:00
codl c322362788
doit: compress twitter.png mastodon.png 2017-08-23 23:10:00 +02:00
codl 1b5e0d5e46
fix cut off mastodon icon 2017-08-23 15:39:52 +02:00
codl 687d2fe96e
nicer log in buttons on index 2017-08-23 15:14:24 +02:00
codl 47ef77d0e2
ugggghhhhhh oops 2017-08-23 11:50:30 +02:00
codl f450eb2c02
yes 2017-08-23 11:49:09 +02:00
codl a52a9c2bb5
oh no oops 2017-08-23 11:45:46 +02:00
codl 40fbea082f
add more granular brotli cache header 2017-08-23 11:42:53 +02:00
codl 1dfb728805
add autocomplete for mastodon instances 2017-08-23 11:42:32 +02:00
codl 5e250a4d03
uhh whoops lol 2017-08-23 11:23:04 +02:00
codl 0e9f732907
footer: link to commits list instead of single commit 2017-08-21 10:56:51 +02:00
codl 9c0d28ad4d
doit: a more radical way to clean up 2017-08-20 20:25:36 +02:00
codl ae19b326af
compress logo as jpeg and webp 2017-08-20 20:17:38 +02:00
codl 78d8c89bd9
oops. remove version.js from doit 2017-08-20 20:15:49 +02:00
codl c036664422
uh oh oops 2017-08-20 18:54:01 +02:00
codl 0360de3d95
😗 2017-08-20 18:52:09 +02:00
codl 069a8ab9fb
shorten footer and link directly to the running commit 2017-08-20 18:48:43 +02:00
codl 6bd769e21e
oops!!!!!! hmm 2017-08-20 18:26:30 +02:00
codl bf115b1176
v0.0.7 2017-08-20 18:23:38 +02:00
codl 06f144f8b4
allow mastodon users to preserve DMs 2017-08-20 18:23:38 +02:00
codl b63f2f2b06
don't store post body 2017-08-20 18:23:38 +02:00
codl fc58833bf5
cachebust: don't 500 on non-existant files 2017-08-20 12:43:29 +02:00
codl 58fd10977e
update index copy
i can't really say 'delete every eligible post' since i cant do that on
mastodon
2017-08-19 16:13:26 +02:00
codl b219f2971b
OOPS oops 2017-08-19 15:17:22 +02:00
codl 0dbfa5e0bc
fix 500 when logging in with mastdon with an existing oauth token 2017-08-19 14:32:31 +02:00
codl 126c24db5f
be smarter about rate limits on mastodon 2017-08-19 14:18:33 +02:00
codl d784f2f01d
increase legibility of log in links 2017-08-19 13:33:42 +02:00
codl f4950a27ff
update front page language for mastodon 2017-08-19 13:27:20 +02:00
codl 8ad6b841c1
fix post-receive hook 2017-08-19 13:21:00 +02:00
codl ff358ed64f
ahhhhhh!! mastodon support 2017-08-19 13:12:22 +02:00
codl d3d93c3cef
ghfjklghjdkflhgjfklhgjkdflshgjdflshgjkdflshgjdfklshgjkdflghjkdflhgjkldfshjgkldfhjgldhfjkgldhfjgklfdhsjgklhfjkslghjdfklsg
mastodon why
2017-08-19 13:12:22 +02:00
codl 340eb711d5
i'm big dummy 2017-08-18 23:03:49 +02:00
codl 557dfe9582
oops 2017-08-18 22:58:41 +02:00
codl c11e4c8dfb
fix settings for empty display name 2017-08-18 22:33:22 +02:00
codl fdaa32e5eb
make favicon transparent 2017-08-18 13:34:53 +02:00
codl 544b780a90
/api/viewers: move timers to a separate route 2017-08-16 00:23:41 +02:00
codl 1791c4065b
indices indexes 2017-08-15 23:58:33 +02:00
codl c32332d07c
d 2017-08-14 22:57:30 +02:00
codl 59c278b3dc
x 2017-08-14 21:22:42 +02:00
codl 1c1ad534a7
FRICK heck h 2017-08-14 21:12:33 +02:00
codl 6d3a6ae936
super mario kart for the super nintendo entertainment system 2017-08-14 21:01:34 +02:00
codl 1cd5b8fa47
replace last_delete with next_delete 2017-08-14 20:58:22 +02:00
codl cebfab2542
add some probably good indexes 2017-08-14 20:30:28 +02:00
codl dc45cddf96
secret debug config option to log all queries shh 🙊 2017-08-14 20:19:51 +02:00
codl c54f6d0eee
tasks: ignore inaccessible accounts for fetch as well 2017-08-14 20:01:59 +02:00
codl 3ebb9476ef
oops 2017-08-13 17:39:05 +02:00
codl 4fa65aa1fe
g 2017-08-13 17:10:53 +02:00
codl 6e39874578
oops 2017-08-13 11:16:43 +02:00
codl 88b0eb121b
disable policy on accounts that have no tokens 2017-08-13 11:11:21 +02:00
codl 1a54f5052f
refresh: take the 100 most stale posts
it doesn't make sense to sample random posts anymore since i don't use
the result of that for picking a post to delete anymore
2017-08-13 11:03:29 +02:00
codl bd574920b4
refresh task: ignore accounts with no tokens 2017-08-13 10:53:29 +02:00
codl 74f20e3138
add a task to refresh the account with the longest time since a refresh 2017-08-12 23:22:22 +02:00
codl efeb5b6f41
extend /api/viewer to include various timers
also add a last_refresh field to accounts
2017-08-12 23:07:16 +02:00
codl 639d209a95
add header showing brotli cache status 2017-08-12 22:01:42 +02:00
codl cdff524e3d
fix 500 on tweet archive upload 2017-08-12 20:32:51 +02:00
codl ed574c530f
also update viewer info when updating counters 2017-08-12 12:27:25 +02:00
codl 578ffb750e
make status display sticky for small screens 2017-08-12 08:29:58 +02:00
codl a4b87ae6b4
fix wonky status display on narrow screens 2017-08-12 07:57:26 +02:00
codl ccb37b04f4
rename settings_form.js to settings.js 2017-08-12 07:26:06 +02:00
codl 9b2273caa8
oops forgot to remove script tag for version.js 2017-08-12 02:05:38 +02:00
codl 7b6a70a7ce
Revert "load version dynamically"
stutid

This reverts commit 51cf43d073.
2017-08-12 02:04:11 +02:00
codl f1d0ed3f0c
add development procfile 2017-08-12 01:54:19 +02:00
codl 8236c4526d
dynamically update post counts 2017-08-12 01:52:33 +02:00
codl d8d31d9b0a
doit: compress js too 2017-08-12 01:07:12 +02:00
126 changed files with 9629 additions and 1046 deletions

3
.codecov.yml Normal file
View File

@ -0,0 +1,3 @@
ignore:
- version.py
comment: off

3
.coveragerc Normal file
View File

@ -0,0 +1,3 @@
[run]
omit =
*/site-packages/*

22
.dockerignore Normal file
View File

@ -0,0 +1,22 @@
.envrc.example
.gitignore
.tool-versions
*.md
Dockerfile
docker-compose.yml
#.git
.github
.codecov.yml
.coveragerc
.env
.eslintrc.yml
.gitattributes
LICENSE
CHANGELOG.markdown
README.markdown
config.example.py
config.docker.py
forget.example.service
requirements-dev.txt
data
config.py

1
.env Normal file
View File

@ -0,0 +1 @@
FLASK_APP=forget.py

17
.eslintrc.yml Normal file
View File

@ -0,0 +1,17 @@
env:
browser: true
es6: true
extends: 'eslint:recommended'
rules:
indent:
- error
- 4
linebreak-style:
- error
- unix
quotes:
- error
- single
semi:
- error
- always

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
version.py export-subst

12
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,12 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: codl
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

15
.github/stale.yml vendored Normal file
View File

@ -0,0 +1,15 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels: []
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs. Thank you
for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

102
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,102 @@
name: Docker
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
on:
schedule:
- cron: '18 10 * * *'
push:
branches: [ "master" ]
# Publish semver tags as releases.
tags: [ 'v*.*.*' ]
pull_request:
branches: [ "master" ]
env:
# Use docker.io for Docker Hub if empty
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
# This is used to complete the identity challenge
# with sigstore/fulcio when running outside of PRs.
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer
- name: Install cosign
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@7e0881f8fe90b25e305bbf0309761e9314607e25
with:
cosign-release: 'v1.9.0'
# Workaround: https://github.com/docker/build-push-action/issues/461
- name: Setup Docker buildx
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
# Login against a Docker registry except on PR
# https://github.com/docker/login-action
- name: Log into registry ${{ env.REGISTRY }}
if: github.event_name != 'pull_request'
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Extract metadata (tags, labels) for Docker
# https://github.com/docker/metadata-action
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
# Build and push Docker image with Buildx (don't push on PR)
# https://github.com/docker/build-push-action
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
# transparency data even for private images, pass --force to cosign below.
# https://github.com/sigstore/cosign
- name: Sign the published Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
COSIGN_EXPERIMENTAL: "true"
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }}

39
.github/workflows/run-tests.yml vendored Normal file
View File

@ -0,0 +1,39 @@
name: Run tests on pushes and on PRs
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
run_tests:
name: Run tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: "3.6"
cache: 'pipenv'
- uses: actions/setup-node@v2
with:
node-version: "10"
cache: 'npm'
- name: Install pipenv and python dependencies
run: |
pip install -U pip pipenv
pipenv sync -d
- name: Install node dependencies
run: npm install
- name: Start Redis
run: docker run --name redis --publish 6379:6379 --detach redis
- name: Run tests with pytest
run: pipenv run pytest --cov=.
- uses: codecov/codecov-action@v2

7
.gitignore vendored
View File

@ -5,4 +5,11 @@ celerybeat-schedule
.doit.db
static/*
!static/.keep
.cache/
.coverage
.pytest_cache
data/*
!data/.keep
docker-compose.override.yml

196
CHANGELOG.markdown Normal file
View File

@ -0,0 +1,196 @@
## v2.2.0
Released 2022-08-10
* add: instance hidelist
<https://github.com/codl/forget/pull/590>
* add: docker deployment support (Thanks @shibaobun !)
<https://github.com/codl/forget/pull/612>
* removed: migration path for known instances list from cookie to localstorage
<https://github.com/codl/forget/pull/545>
## v2.1.0
Released 2022-03-04
* 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
<https://github.com/codl/forget/pull/366>
* fix: failing to fetch some posts in some circumstances
<https://github.com/codl/forget/issue/584>
## v2.0.0
Released 2019-09-13
* fix: newer twitter accounts not fetching new posts past the initial historical fetch
<https://github.com/codl/forget/pull/254>
* fix: fetching task not getting rescheduled properly when doing the initial historic fetch
<https://github.com/codl/forget/pull/264>
* BREAKING: disabled tweet archive upload since twitter has dropped support for them
<https://github.com/codl/forget/pull/265>
## v1.6.1
Released 2019-07-23
* increased frequency of refresh jobs
* version number in footer now links to changelog instead of commit log
* updated about page. less negative, more succint, clarifies that forget is not a purging tool
## v1.5.3
Released 2019-07-11
* fix: everything related to mastodon broken because of a typo
## v1.5.2
Released 2019-07-11
* fix: stock user agent when querying mastodon servers
## v1.5.1
Released 2019-05-02
* fix: proxied avatars not working in some environments
* dependency updates
## v1.5.0
Released 2019-03-15
* back off before hitting rate limit on mastodon instances
* tracking of used instances for the login buttons on the front page is now entirely client-side,
avoiding a potential information disclosure vulnerability ([GH-175](https://github.com/codl/forget/issues/175))
* fix: fetch\_acc running multiple copies fetching the same posts
* internals: increased frequency of refresh jobs, decreased frequency of bookkeeping jobs
## v1.4.3
Released 2019-03-11
* documentation improvements
* fix: deadlock when refreshing or deleting from mastodon accounts ([GH-19](https://github.com/codl/forget/issues/19))
* fix: crash in fetch\_acc when user has no posts
* fix: not backing off if something crashes in refresh\_account
* fix: crashes when trying to refresh but no accounts have been created yet
## v1.4.2
Released 2019-02-24
* fix: implemented a more robust fetching algorithm, which should prevent accounts getting stuck with only a fraction of their posts fetched ([GH-13](https://github.com/codl/forget/issues/13))
* fix: picture tags having an extra comma
* fix: outdated joke in about page
* fix: posts' status not getting refreshed (ie whether or not they were faved, or deleted externally)
* internals: removed `x-` prefix from custom headers, as per [section 8.3.1 of RFC7231](https://httpwg.org/specs/rfc7231.html#considerations.for.new.header.fields)
## v1.4.1 (security update)
Released 2018-10-29
* updated requests to 2.20.0 ([CVE-2018-18074](https://nvd.nist.gov/vuln/detail/CVE-2018-18074))
## v1.4.0
Released 2018-10-06
* added warning when it looks like an archive is a full "Your Twitter data" archive
## v1.3.0
Released 2018-07-06
* implement exponential backoff
## v1.2.1
Released 2018-05-08
* limit number of log-in buttons to 5, and show up to 5 known instances
## v1.2.0
Released 2018-05-08
* remember a user's mastodon instances and let them log in in one click ([GH-36](https://github.com/codl/forget/issues/36))
## v1.1.3
Released 2018-04-25
* made radio strips more accessible
* unified button looks
* updated and cleaned up markup in README
## v1.1.2
Released 2018-04-25
* fixed crash when saving settings with JS disabled
## v1.1.1
Released 2018-04-19
* rewrote post-receive hook so it would play nice with versioneer
## v1.1.0
Released 2018-01-31
* three types of policies are now available for favs and media (keep only, delete only, ignore)
* a new input type was introduced to avoid having messy inline radio buttons
* sentry js init file now has 1 hour of caching
* fav and reblog count are now stored, for GH-7
* GH-17 reblogs are deleted regardless of media and favs
* mastodon instance popularity scoring has been simplified
## v1.0.0
* image proxy now respects max-age from cache-control header
* image proxy now stores a handful of whitelisted headers
* privacy policy moved to its own page
* only one copy of each task+args can run at once
* fix Error returning to forget after cancelling authorization #14
* a whole lot of trying to not hit rate limits
* removed flask-limiter
* a whole buncha minor changes and fixes that i don't remember because i'm writing this after the fact 🤷
## v0.0.10
* a test suite (it only tests libbrotli for now)
* an image proxy for those avatars that are served over http not https
* show a message to the user when their account has been
administratively disabled to explain why
* whjole lot of quality of life improvements
* whole lot of bug fixes
* some stylistic changes
## v0.0.9
* logged in page now shows time of last delete and next delete
* enabling/disabling doesnt require a refresh anymore
* security enhancements (A+ on moz observatory binchhhhh)
* bug fixes etc
## v0.0.8
* quick log-in buttons for popular mastodon instances
* add csrf tokens
* bug fixes
## v0.0.7
* add option for mastodon users to preserve direct messages (enabled by default)
* removed storing the posts' bodies. it was convenient for debugging early on but now it's kinda iffy privacy wise
* various fixes for mastodon
## before v0.0.7
idk

51
Dockerfile Normal file
View File

@ -0,0 +1,51 @@
# syntax=docker/dockerfile:1.4.2
FROM python:3.10.6-bullseye AS pydeps
WORKDIR /usr/src/app
RUN --mount=type=cache,target=/root/.cache/pip/http pip install --upgrade pip==22.2.2
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip/http pip install -r requirements.txt
FROM pydeps AS pynodedeps
RUN rm -f /etc/apt/apt.conf.d/docker-clean &&\
echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' \
> /etc/apt/apt.conf.d/keep-cache
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update -qq && apt-get install -qq nodejs npm
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm clean-install
FROM scratch AS layer-cake
WORKDIR /
COPY *.py setup.cfg rollup.config.js ./
COPY assets assets
COPY components components
COPY libforget libforget
COPY migrations migrations
COPY routes routes
COPY static static
COPY templates templates
FROM pynodedeps AS build
COPY --from=layer-cake / ./
RUN doit
FROM pydeps
COPY --from=build /usr/src/app ./
COPY .git/ .git/
ENV FLASK_APP=forget.py
VOLUME ["/var/run/celery"]

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include versioneer.py
include version.py

38
Pipfile Normal file
View File

@ -0,0 +1,38 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
alembic = "*"
brotli = ">=1.0.1"
#celery = "~=4.4.2"
celery = "*"
csscompressor = "*"
doit = "*"
flask = ">=1.1"
flask-migrate = "*"
flask-sqlalchemy = "*"
gunicorn = ">=19.8"
honcho = "*"
pillow = "*"
"psycopg2" = "*"
raven = "*"
redis = "*"
requests = "*"
sqlalchemy = "*"
twitter = "*"
"mastodon.py" = ">=1.2"
blinker = "*"
[dev-packages]
coverage = "*"
codecov = "*"
pytest = "*"
pytest-cov = "*"
versioneer = "*"

876
Pipfile.lock generated Normal file
View File

@ -0,0 +1,876 @@
{
"_meta": {
"hash": {
"sha256": "7247d712fbaff173d8cc9dd7d1bd235eb1f45354c550090b0e96358beee58ea7"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"alembic": {
"hashes": [
"sha256:6c0c05e9768a896d804387e20b299880fe01bc56484246b0dffe8075d6d3d847",
"sha256:ad842f2c3ab5c5d4861232730779c05e33db4ba880a08b85eb505e87c01095bc"
],
"index": "pypi",
"version": "==1.7.6"
},
"amqp": {
"hashes": [
"sha256:1e5f707424e544078ca196e72ae6a14887ce74e02bd126be54b7c03c971bef18",
"sha256:9cd81f7b023fc04bbb108718fbac674f06901b77bfcdce85b10e2a5d0ee91be5"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.9"
},
"billiard": {
"hashes": [
"sha256:299de5a8da28a783d51b197d496bef4f1595dd023a93a4f59dde1886ae905547",
"sha256:87103ea78fa6ab4d5c751c4909bcff74617d985de7fa8b672cf8618afd5a875b"
],
"version": "==3.6.4.0"
},
"blinker": {
"hashes": [
"sha256:471aee25f3992bd325afa3772f1063dbdbbca947a041b8b89466dc00d606f8b6"
],
"index": "pypi",
"version": "==1.4"
},
"blurhash": {
"hashes": [
"sha256:7611c1bc41383d2349b6129208587b5d61e8792ce953893cb49c38beeb400d1d",
"sha256:da56b163e5a816e4ad07172f5639287698e09d7f3dc38d18d9726d9c1dbc4cee"
],
"version": "==1.1.4"
},
"brotli": {
"hashes": [
"sha256:12effe280b8ebfd389022aa65114e30407540ccb89b177d3fbc9a4f177c4bd5d",
"sha256:160c78292e98d21e73a4cc7f76a234390e516afcd982fa17e1422f7c6a9ce9c8",
"sha256:16d528a45c2e1909c2798f27f7bf0a3feec1dc9e50948e738b961618e38b6a7b",
"sha256:19598ecddd8a212aedb1ffa15763dd52a388518c4550e615aed88dc3753c0f0c",
"sha256:1c48472a6ba3b113452355b9af0a60da5c2ae60477f8feda8346f8fd48e3e87c",
"sha256:268fe94547ba25b58ebc724680609c8ee3e5a843202e9a381f6f9c5e8bdb5c70",
"sha256:269a5743a393c65db46a7bb982644c67ecba4b8d91b392403ad8a861ba6f495f",
"sha256:26d168aac4aaec9a4394221240e8a5436b5634adc3cd1cdf637f6645cecbf181",
"sha256:29d1d350178e5225397e28ea1b7aca3648fcbab546d20e7475805437bfb0a130",
"sha256:2aad0e0baa04517741c9bb5b07586c642302e5fb3e75319cb62087bd0995ab19",
"sha256:3496fc835370da351d37cada4cf744039616a6db7d13c430035e901443a34daa",
"sha256:35a3edbe18e876e596553c4007a087f8bcfd538f19bc116917b3c7522fca0429",
"sha256:3b78a24b5fd13c03ee2b7b86290ed20efdc95da75a3557cc06811764d5ad1126",
"sha256:40d15c79f42e0a2c72892bf407979febd9cf91f36f495ffb333d1d04cebb34e4",
"sha256:44bb8ff420c1d19d91d79d8c3574b8954288bdff0273bf788954064d260d7ab0",
"sha256:4688c1e42968ba52e57d8670ad2306fe92e0169c6f3af0089be75bbac0c64a3b",
"sha256:495ba7e49c2db22b046a53b469bbecea802efce200dffb69b93dd47397edc9b6",
"sha256:4d1b810aa0ed773f81dceda2cc7b403d01057458730e309856356d4ef4188438",
"sha256:503fa6af7da9f4b5780bb7e4cbe0c639b010f12be85d02c99452825dd0feef3f",
"sha256:56d027eace784738457437df7331965473f2c0da2c70e1a1f6fdbae5402e0389",
"sha256:5913a1177fc36e30fcf6dc868ce23b0453952c78c04c266d3149b3d39e1410d6",
"sha256:5b6ef7d9f9c38292df3690fe3e302b5b530999fa90014853dcd0d6902fb59f26",
"sha256:5cb1e18167792d7d21e21365d7650b72d5081ed476123ff7b8cac7f45189c0c7",
"sha256:61a7ee1f13ab913897dac7da44a73c6d44d48a4adff42a5701e3239791c96e14",
"sha256:622a231b08899c864eb87e85f81c75e7b9ce05b001e59bbfbf43d4a71f5f32b2",
"sha256:68715970f16b6e92c574c30747c95cf8cf62804569647386ff032195dc89a430",
"sha256:6b2ae9f5f67f89aade1fab0f7fd8f2832501311c363a21579d02defa844d9296",
"sha256:6c772d6c0a79ac0f414a9f8947cc407e119b8598de7621f39cacadae3cf57d12",
"sha256:6d847b14f7ea89f6ad3c9e3901d1bc4835f6b390a9c71df999b0162d9bb1e20f",
"sha256:76ffebb907bec09ff511bb3acc077695e2c32bc2142819491579a695f77ffd4d",
"sha256:7bbff90b63328013e1e8cb50650ae0b9bac54ffb4be6104378490193cd60f85a",
"sha256:7cb81373984cc0e4682f31bc3d6be9026006d96eecd07ea49aafb06897746452",
"sha256:7ee83d3e3a024a9618e5be64648d6d11c37047ac48adff25f12fa4226cf23d1c",
"sha256:854c33dad5ba0fbd6ab69185fec8dab89e13cda6b7d191ba111987df74f38761",
"sha256:85f7912459c67eaab2fb854ed2bc1cc25772b300545fe7ed2dc03954da638649",
"sha256:87fdccbb6bb589095f413b1e05734ba492c962b4a45a13ff3408fa44ffe6479b",
"sha256:88c63a1b55f352b02c6ffd24b15ead9fc0e8bf781dbe070213039324922a2eea",
"sha256:8a674ac10e0a87b683f4fa2b6fa41090edfd686a6524bd8dedbd6138b309175c",
"sha256:93130612b837103e15ac3f9cbacb4613f9e348b58b3aad53721d92e57f96d46a",
"sha256:9744a863b489c79a73aba014df554b0e7a0fc44ef3f8a0ef2a52919c7d155031",
"sha256:9749a124280a0ada4187a6cfd1ffd35c350fb3af79c706589d98e088c5044267",
"sha256:97f715cf371b16ac88b8c19da00029804e20e25f30d80203417255d239f228b5",
"sha256:9bf919756d25e4114ace16a8ce91eb340eb57a08e2c6950c3cebcbe3dff2a5e7",
"sha256:9d12cf2851759b8de8ca5fde36a59c08210a97ffca0eb94c532ce7b17c6a3d1d",
"sha256:9ed4c92a0665002ff8ea852353aeb60d9141eb04109e88928026d3c8a9e5433c",
"sha256:a72661af47119a80d82fa583b554095308d6a4c356b2a554fdc2799bc19f2a43",
"sha256:afde17ae04d90fbe53afb628f7f2d4ca022797aa093e809de5c3cf276f61bbfa",
"sha256:b336c5e9cf03c7be40c47b5fd694c43c9f1358a80ba384a21969e0b4e66a9b17",
"sha256:b663f1e02de5d0573610756398e44c130add0eb9a3fc912a09665332942a2efb",
"sha256:b83bb06a0192cccf1eb8d0a28672a1b79c74c3a8a5f2619625aeb6f28b3a82bb",
"sha256:c2415d9d082152460f2bd4e382a1e85aed233abc92db5a3880da2257dc7daf7b",
"sha256:c83aa123d56f2e060644427a882a36b3c12db93727ad7a7b9efd7d7f3e9cc2c4",
"sha256:cfc391f4429ee0a9370aa93d812a52e1fee0f37a81861f4fdd1f4fb28e8547c3",
"sha256:db844eb158a87ccab83e868a762ea8024ae27337fc7ddcbfcddd157f841fdfe7",
"sha256:defed7ea5f218a9f2336301e6fd379f55c655bea65ba2476346340a0ce6f74a1",
"sha256:e16eb9541f3dd1a3e92b89005e37b1257b157b7256df0e36bd7b33b50be73bcb",
"sha256:e23281b9a08ec338469268f98f194658abfb13658ee98e2b7f85ee9dd06caa91",
"sha256:e2d9e1cbc1b25e22000328702b014227737756f4b5bf5c485ac1d8091ada078b",
"sha256:e48f4234f2469ed012a98f4b7874e7f7e173c167bed4934912a29e03167cf6b1",
"sha256:e4c4e92c14a57c9bd4cb4be678c25369bf7a092d55fd0866f759e425b9660806",
"sha256:ec1947eabbaf8e0531e8e899fc1d9876c179fc518989461f5d24e2223395a9e3",
"sha256:f909bbbc433048b499cb9db9e713b5d8d949e8c109a2a548502fb9aa8630f0b1"
],
"index": "pypi",
"version": "==1.0.9"
},
"celery": {
"hashes": [
"sha256:8aacd02fc23a02760686d63dde1eb0daa9f594e735e73ea8fb15c2ff15cb608c",
"sha256:e2cd41667ad97d4f6a2f4672d1c6a6ebada194c619253058b5f23704aaadaa82"
],
"index": "pypi",
"version": "==5.2.3"
},
"certifi": {
"hashes": [
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
"version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_version >= '3'",
"version": "==2.0.12"
},
"click": {
"hashes": [
"sha256:6a7a62563bbfabfda3a38f3023a1db4a35978c0abd76f6c9605ecd6554d6d9b1",
"sha256:8458d7b1287c5fb128c90e23381cf99dcde74beaf6c7ff6384ce84d6fe090adb"
],
"markers": "python_version >= '3.6'",
"version": "==8.0.4"
},
"click-didyoumean": {
"hashes": [
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
],
"markers": "python_version < '4' and python_full_version >= '3.6.2'",
"version": "==0.3.0"
},
"click-plugins": {
"hashes": [
"sha256:46ab999744a9d831159c3411bb0c79346d94a444df9a3a3742e9ed63645f264b",
"sha256:5d262006d3222f5057fd81e1623d4443e41dcda5dc815c06b442aa3c02889fc8"
],
"version": "==1.1.1"
},
"click-repl": {
"hashes": [
"sha256:94b3fbbc9406a236f176e0506524b2937e4b23b6f4c0c0b2a0a83f8a64e9194b",
"sha256:cd12f68d745bf6151210790540b4cb064c7b13e571bc64b6957d98d120dacfd8"
],
"version": "==0.2.0"
},
"cloudpickle": {
"hashes": [
"sha256:5cd02f3b417a783ba84a4ec3e290ff7929009fe51f6405423cfccfadd43ba4a4",
"sha256:6b2df9741d06f43839a3275c4e6632f7df6487a1f181f5f46a052d3c917c3d11"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.0"
},
"csscompressor": {
"hashes": [
"sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05"
],
"index": "pypi",
"version": "==0.9.5"
},
"decorator": {
"hashes": [
"sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330",
"sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"
],
"markers": "python_version >= '3.5'",
"version": "==5.1.1"
},
"deprecated": {
"hashes": [
"sha256:43ac5335da90c31c24ba028af536a91d41d53f9e6901ddb021bcc572ce44e38d",
"sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.2.13"
},
"doit": {
"hashes": [
"sha256:388111f8a6a5b3b44620ceeb287d0585927c85d0d6d3f8790d34fbb45c0cf6c3",
"sha256:a7f414de4c596a96a727890e0792709a103f1af48e8180e27e07862fff781238"
],
"index": "pypi",
"version": "==0.34.2"
},
"flask": {
"hashes": [
"sha256:59da8a3170004800a2837844bfa84d49b022550616070f7cb1a659682b2e7c9f",
"sha256:e1120c228ca2f553b470df4a5fa927ab66258467526069981b3eb0a91902687d"
],
"index": "pypi",
"version": "==2.0.3"
},
"flask-migrate": {
"hashes": [
"sha256:57d6060839e3a7f150eaab6fe4e726d9e3e7cffe2150fb223d73f92421c6d1d9",
"sha256:a6498706241aba6be7a251078de9cf166d74307bca41a4ca3e403c9d39e2f897"
],
"index": "pypi",
"version": "==3.1.0"
},
"flask-sqlalchemy": {
"hashes": [
"sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912",
"sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390"
],
"index": "pypi",
"version": "==2.5.1"
},
"greenlet": {
"hashes": [
"sha256:0051c6f1f27cb756ffc0ffbac7d2cd48cb0362ac1736871399a739b2885134d3",
"sha256:00e44c8afdbe5467e4f7b5851be223be68adb4272f44696ee71fe46b7036a711",
"sha256:013d61294b6cd8fe3242932c1c5e36e5d1db2c8afb58606c5a67efce62c1f5fd",
"sha256:049fe7579230e44daef03a259faa24511d10ebfa44f69411d99e6a184fe68073",
"sha256:14d4f3cd4e8b524ae9b8aa567858beed70c392fdec26dbdb0a8a418392e71708",
"sha256:166eac03e48784a6a6e0e5f041cfebb1ab400b394db188c48b3a84737f505b67",
"sha256:17ff94e7a83aa8671a25bf5b59326ec26da379ace2ebc4411d690d80a7fbcf23",
"sha256:1e12bdc622676ce47ae9abbf455c189e442afdde8818d9da983085df6312e7a1",
"sha256:21915eb821a6b3d9d8eefdaf57d6c345b970ad722f856cd71739493ce003ad08",
"sha256:288c6a76705dc54fba69fbcb59904ae4ad768b4c768839b8ca5fdadec6dd8cfd",
"sha256:2bde6792f313f4e918caabc46532aa64aa27a0db05d75b20edfc5c6f46479de2",
"sha256:32ca72bbc673adbcfecb935bb3fb1b74e663d10a4b241aaa2f5a75fe1d1f90aa",
"sha256:356b3576ad078c89a6107caa9c50cc14e98e3a6c4874a37c3e0273e4baf33de8",
"sha256:40b951f601af999a8bf2ce8c71e8aaa4e8c6f78ff8afae7b808aae2dc50d4c40",
"sha256:572e1787d1460da79590bf44304abbc0a2da944ea64ec549188fa84d89bba7ab",
"sha256:58df5c2a0e293bf665a51f8a100d3e9956febfbf1d9aaf8c0677cf70218910c6",
"sha256:64e6175c2e53195278d7388c454e0b30997573f3f4bd63697f88d855f7a6a1fc",
"sha256:7227b47e73dedaa513cdebb98469705ef0d66eb5a1250144468e9c3097d6b59b",
"sha256:7418b6bfc7fe3331541b84bb2141c9baf1ec7132a7ecd9f375912eca810e714e",
"sha256:7cbd7574ce8e138bda9df4efc6bf2ab8572c9aff640d8ecfece1b006b68da963",
"sha256:7ff61ff178250f9bb3cd89752df0f1dd0e27316a8bd1465351652b1b4a4cdfd3",
"sha256:833e1551925ed51e6b44c800e71e77dacd7e49181fdc9ac9a0bf3714d515785d",
"sha256:8639cadfda96737427330a094476d4c7a56ac03de7265622fcf4cfe57c8ae18d",
"sha256:8c5d5b35f789a030ebb95bff352f1d27a93d81069f2adb3182d99882e095cefe",
"sha256:8c790abda465726cfb8bb08bd4ca9a5d0a7bd77c7ac1ca1b839ad823b948ea28",
"sha256:8d2f1fb53a421b410751887eb4ff21386d119ef9cde3797bf5e7ed49fb51a3b3",
"sha256:903bbd302a2378f984aef528f76d4c9b1748f318fe1294961c072bdc7f2ffa3e",
"sha256:93f81b134a165cc17123626ab8da2e30c0455441d4ab5576eed73a64c025b25c",
"sha256:95e69877983ea39b7303570fa6760f81a3eec23d0e3ab2021b7144b94d06202d",
"sha256:9633b3034d3d901f0a46b7939f8c4d64427dfba6bbc5a36b1a67364cf148a1b0",
"sha256:97e5306482182170ade15c4b0d8386ded995a07d7cc2ca8f27958d34d6736497",
"sha256:9f3cba480d3deb69f6ee2c1825060177a22c7826431458c697df88e6aeb3caee",
"sha256:aa5b467f15e78b82257319aebc78dd2915e4c1436c3c0d1ad6f53e47ba6e2713",
"sha256:abb7a75ed8b968f3061327c433a0fbd17b729947b400747c334a9c29a9af6c58",
"sha256:aec52725173bd3a7b56fe91bc56eccb26fbdff1386ef123abb63c84c5b43b63a",
"sha256:b11548073a2213d950c3f671aa88e6f83cda6e2fb97a8b6317b1b5b33d850e06",
"sha256:b1692f7d6bc45e3200844be0dba153612103db241691088626a33ff1f24a0d88",
"sha256:b336501a05e13b616ef81ce329c0e09ac5ed8c732d9ba7e3e983fcc1a9e86965",
"sha256:b8c008de9d0daba7b6666aa5bbfdc23dcd78cafc33997c9b7741ff6353bafb7f",
"sha256:b92e29e58bef6d9cfd340c72b04d74c4b4e9f70c9fa7c78b674d1fec18896dc4",
"sha256:be5f425ff1f5f4b3c1e33ad64ab994eed12fc284a6ea71c5243fd564502ecbe5",
"sha256:dd0b1e9e891f69e7675ba5c92e28b90eaa045f6ab134ffe70b52e948aa175b3c",
"sha256:e30f5ea4ae2346e62cedde8794a56858a67b878dd79f7df76a0767e356b1744a",
"sha256:e6a36bb9474218c7a5b27ae476035497a6990e21d04c279884eb10d9b290f1b1",
"sha256:e859fcb4cbe93504ea18008d1df98dee4f7766db66c435e4882ab35cf70cac43",
"sha256:eb6ea6da4c787111adf40f697b4e58732ee0942b5d3bd8f435277643329ba627",
"sha256:ec8c433b3ab0419100bd45b47c9c8551248a5aee30ca5e9d399a0b57ac04651b",
"sha256:eff9d20417ff9dcb0d25e2defc2574d10b491bf2e693b4e491914738b7908168",
"sha256:f0214eb2a23b85528310dad848ad2ac58e735612929c8072f6093f3585fd342d",
"sha256:f276df9830dba7a333544bd41070e8175762a7ac20350786b322b714b0e654f5",
"sha256:f3acda1924472472ddd60c29e5b9db0cec629fbe3c5c5accb74d6d6d14773478",
"sha256:f70a9e237bb792c7cc7e44c531fd48f5897961701cdaa06cf22fc14965c496cf",
"sha256:f9d29ca8a77117315101425ec7ec2a47a22ccf59f5593378fc4077ac5b754fce",
"sha256:fa877ca7f6b48054f847b61d6fa7bed5cebb663ebc55e018fda12db09dcc664c",
"sha256:fdcec0b8399108577ec290f55551d926d9a1fa6cad45882093a7a07ac5ec147b"
],
"markers": "python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))",
"version": "==1.1.2"
},
"gunicorn": {
"hashes": [
"sha256:9dcc4547dbb1cb284accfb15ab5667a0e5d1881cc443e0677b4882a4067a807e",
"sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"
],
"index": "pypi",
"version": "==20.1.0"
},
"honcho": {
"hashes": [
"sha256:a4d6e3a88a7b51b66351ecfc6e9d79d8f4b87351db9ad7e923f5632cc498122f",
"sha256:c5eca0bded4bef6697a23aec0422fd4f6508ea3581979a3485fc4b89357eb2a9"
],
"index": "pypi",
"version": "==1.1.0"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"version": "==3.3"
},
"itsdangerous": {
"hashes": [
"sha256:29285842166554469a56d427addc0843914172343784cb909695fdbe90a3e129",
"sha256:d848fcb8bc7d507c4546b448574e8a44fc4ea2ba84ebf8d783290d53e81992f5"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.0"
},
"jinja2": {
"hashes": [
"sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8",
"sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.3"
},
"kombu": {
"hashes": [
"sha256:81a90c1de97e08d3db37dbf163eaaf667445e1068c98bfd89f051a40e9f6dbbd",
"sha256:eeaeb8024f3a5cfc71c9250e45cddb8493f269d74ada2f74909a93c59c4b4179"
],
"markers": "python_version >= '3.7'",
"version": "==5.2.3"
},
"mako": {
"hashes": [
"sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2",
"sha256:afaf8e515d075b22fad7d7b8b30e4a1c90624ff2f3733a06ec125f5a5f043a57"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.1.6"
},
"markupsafe": {
"hashes": [
"sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3",
"sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8",
"sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759",
"sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed",
"sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989",
"sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3",
"sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a",
"sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c",
"sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c",
"sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8",
"sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454",
"sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad",
"sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d",
"sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635",
"sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61",
"sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea",
"sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49",
"sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce",
"sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e",
"sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f",
"sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f",
"sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f",
"sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7",
"sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a",
"sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7",
"sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076",
"sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb",
"sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7",
"sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7",
"sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c",
"sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26",
"sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c",
"sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8",
"sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448",
"sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956",
"sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05",
"sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1",
"sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357",
"sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea",
"sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.0"
},
"mastodon.py": {
"hashes": [
"sha256:2afddbad8b5d7326fcc8a8f8c62bfe956e34627f516b06c6694fc8c8fedc33ee",
"sha256:cc454cac0ed1ae4f105f7399ea53f5b31a1be5075d1882f47162d2e78a9e4064"
],
"index": "pypi",
"version": "==1.5.1"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"pillow": {
"hashes": [
"sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97",
"sha256:0f29d831e2151e0b7b39981756d201f7108d3d215896212ffe2e992d06bfe049",
"sha256:12875d118f21cf35604176872447cdb57b07126750a33748bac15e77f90f1f9c",
"sha256:14d4b1341ac07ae07eb2cc682f459bec932a380c3b122f5540432d8977e64eae",
"sha256:1c3c33ac69cf059bbb9d1a71eeaba76781b450bc307e2291f8a4764d779a6b28",
"sha256:1d19397351f73a88904ad1aee421e800fe4bbcd1aeee6435fb62d0a05ccd1030",
"sha256:253e8a302a96df6927310a9d44e6103055e8fb96a6822f8b7f514bb7ef77de56",
"sha256:2632d0f846b7c7600edf53c48f8f9f1e13e62f66a6dbc15191029d950bfed976",
"sha256:335ace1a22325395c4ea88e00ba3dc89ca029bd66bd5a3c382d53e44f0ccd77e",
"sha256:413ce0bbf9fc6278b2d63309dfeefe452835e1c78398efb431bab0672fe9274e",
"sha256:5100b45a4638e3c00e4d2320d3193bdabb2d75e79793af7c3eb139e4f569f16f",
"sha256:514ceac913076feefbeaf89771fd6febde78b0c4c1b23aaeab082c41c694e81b",
"sha256:528a2a692c65dd5cafc130de286030af251d2ee0483a5bf50c9348aefe834e8a",
"sha256:6295f6763749b89c994fcb6d8a7f7ce03c3992e695f89f00b741b4580b199b7e",
"sha256:6c8bc8238a7dfdaf7a75f5ec5a663f4173f8c367e5a39f87e720495e1eed75fa",
"sha256:718856856ba31f14f13ba885ff13874be7fefc53984d2832458f12c38205f7f7",
"sha256:7f7609a718b177bf171ac93cea9fd2ddc0e03e84d8fa4e887bdfc39671d46b00",
"sha256:80ca33961ced9c63358056bd08403ff866512038883e74f3a4bf88ad3eb66838",
"sha256:80fe64a6deb6fcfdf7b8386f2cf216d329be6f2781f7d90304351811fb591360",
"sha256:81c4b81611e3a3cb30e59b0cf05b888c675f97e3adb2c8672c3154047980726b",
"sha256:855c583f268edde09474b081e3ddcd5cf3b20c12f26e0d434e1386cc5d318e7a",
"sha256:9bfdb82cdfeccec50aad441afc332faf8606dfa5e8efd18a6692b5d6e79f00fd",
"sha256:a5d24e1d674dd9d72c66ad3ea9131322819ff86250b30dc5821cbafcfa0b96b4",
"sha256:a9f44cd7e162ac6191491d7249cceb02b8116b0f7e847ee33f739d7cb1ea1f70",
"sha256:b5b3f092fe345c03bca1e0b687dfbb39364b21ebb8ba90e3fa707374b7915204",
"sha256:b9618823bd237c0d2575283f2939655f54d51b4527ec3972907a927acbcc5bfc",
"sha256:cef9c85ccbe9bee00909758936ea841ef12035296c748aaceee535969e27d31b",
"sha256:d21237d0cd37acded35154e29aec853e945950321dd2ffd1a7d86fe686814669",
"sha256:d3c5c79ab7dfce6d88f1ba639b77e77a17ea33a01b07b99840d6ed08031cb2a7",
"sha256:d9d7942b624b04b895cb95af03a23407f17646815495ce4547f0e60e0b06f58e",
"sha256:db6d9fac65bd08cea7f3540b899977c6dee9edad959fa4eaf305940d9cbd861c",
"sha256:ede5af4a2702444a832a800b8eb7f0a7a1c0eed55b644642e049c98d589e5092",
"sha256:effb7749713d5317478bb3acb3f81d9d7c7f86726d41c1facca068a04cf5bb4c",
"sha256:f154d173286a5d1863637a7dcd8c3437bb557520b01bddb0be0258dcb72696b5",
"sha256:f25ed6e28ddf50de7e7ea99d7a976d6a9c415f03adcaac9c41ff6ff41b6d86ac"
],
"index": "pypi",
"version": "==9.0.1"
},
"prompt-toolkit": {
"hashes": [
"sha256:30129d870dcb0b3b6a53efdc9d0a83ea96162ffd28ffe077e94215b233dc670c",
"sha256:9f1cd16b1e86c2968f2519d7fb31dd9d669916f515612c269d14e9ed52b51650"
],
"markers": "python_full_version >= '3.6.2'",
"version": "==3.0.28"
},
"psycopg2": {
"hashes": [
"sha256:06f32425949bd5fe8f625c49f17ebb9784e1e4fe928b7cce72edc36fb68e4c0c",
"sha256:0762c27d018edbcb2d34d51596e4346c983bd27c330218c56c4dc25ef7e819bf",
"sha256:083707a696e5e1c330af2508d8fab36f9700b26621ccbcb538abe22e15485362",
"sha256:34b33e0162cfcaad151f249c2649fd1030010c16f4bbc40a604c1cb77173dcf7",
"sha256:4295093a6ae3434d33ec6baab4ca5512a5082cc43c0505293087b8a46d108461",
"sha256:8cf3878353cc04b053822896bc4922b194792df9df2f1ad8da01fb3043602126",
"sha256:8e841d1bf3434da985cc5ef13e6f75c8981ced601fd70cc6bf33351b91562981",
"sha256:9572e08b50aed176ef6d66f15a21d823bb6f6d23152d35e8451d7d2d18fdac56",
"sha256:a81e3866f99382dfe8c15a151f1ca5fde5815fde879348fe5a9884a7c092a305",
"sha256:cb10d44e6694d763fa1078a26f7f6137d69f555a78ec85dc2ef716c37447e4b2",
"sha256:d3ca6421b942f60c008f81a3541e8faf6865a28d5a9b48544b0ee4f40cac7fca"
],
"index": "pypi",
"version": "==2.9.3"
},
"pyinotify": {
"hashes": [
"sha256:9c998a5d7606ca835065cdabc013ae6c66eb9ea76a00a1e3bc6e0cfe2b4f71f4"
],
"markers": "sys_platform == 'linux'",
"version": "==0.9.6"
},
"pyparsing": {
"hashes": [
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
"python-magic": {
"hashes": [
"sha256:1a2c81e8f395c744536369790bd75094665e9644110a6623bcc3bbea30f03973",
"sha256:21f5f542aa0330f5c8a64442528542f6215c8e18d2466b399b0d9d39356d83fc"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==0.4.25"
},
"pytz": {
"hashes": [
"sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c",
"sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"
],
"version": "==2021.3"
},
"raven": {
"hashes": [
"sha256:3fa6de6efa2493a7c827472e984ce9b020797d0da16f1db67197bcc23c8fae54",
"sha256:44a13f87670836e153951af9a3c80405d36b43097db869a36e92809673692ce4"
],
"index": "pypi",
"version": "==6.10.0"
},
"redis": {
"hashes": [
"sha256:04629f8e42be942c4f7d1812f2094568f04c612865ad19ad3ace3005da70631a",
"sha256:1d9a0cdf89fdd93f84261733e24f55a7bbd413a9b219fdaf56e3e728ca9a2306"
],
"index": "pypi",
"version": "==4.1.4"
},
"requests": {
"hashes": [
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
],
"index": "pypi",
"version": "==2.27.1"
},
"setuptools": {
"hashes": [
"sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373",
"sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e"
],
"markers": "python_version >= '3.6'",
"version": "==59.6.0"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"sqlalchemy": {
"hashes": [
"sha256:05fa14f279d43df68964ad066f653193187909950aa0163320b728edfc400167",
"sha256:0ddc5e5ccc0160e7ad190e5c61eb57560f38559e22586955f205e537cda26034",
"sha256:15a03261aa1e68f208e71ae3cd845b00063d242cbf8c87348a0c2c0fc6e1f2ac",
"sha256:289465162b1fa1e7a982f8abe59d26a8331211cad4942e8031d2b7db1f75e649",
"sha256:2e216c13ecc7fcdcbb86bb3225425b3ed338e43a8810c7089ddb472676124b9b",
"sha256:2fd4d3ca64c41dae31228b80556ab55b6489275fb204827f6560b65f95692cf3",
"sha256:330eb45395874cc7787214fdd4489e2afb931bc49e0a7a8f9cd56d6e9c5b1639",
"sha256:3c7ed6c69debaf6198fadb1c16ae1253a29a7670bbf0646f92582eb465a0b999",
"sha256:4ad31cec8b49fd718470328ad9711f4dc703507d434fd45461096da0a7135ee0",
"sha256:57205844f246bab9b666a32f59b046add8995c665d9ecb2b7b837b087df90639",
"sha256:582b59d1e5780a447aada22b461e50b404a9dc05768da1d87368ad8190468418",
"sha256:5e9c7b3567edbc2183607f7d9f3e7e89355b8f8984eec4d2cd1e1513c8f7b43f",
"sha256:6a01ec49ca54ce03bc14e10de55dfc64187a2194b3b0e5ac0fdbe9b24767e79e",
"sha256:6f22c040d196f841168b1456e77c30a18a3dc16b336ddbc5a24ce01ab4e95ae0",
"sha256:81f2dd355b57770fdf292b54f3e0a9823ec27a543f947fa2eb4ec0df44f35f0d",
"sha256:85e4c244e1de056d48dae466e9baf9437980c19fcde493e0db1a0a986e6d75b4",
"sha256:8d0949b11681380b4a50ac3cd075e4816afe9fa4a8c8ae006c1ca26f0fa40ad8",
"sha256:975f5c0793892c634c4920057da0de3a48bbbbd0a5c86f5fcf2f2fedf41b76da",
"sha256:9e4fb2895b83993831ba2401b6404de953fdbfa9d7d4fa6a4756294a83bbc94f",
"sha256:b35dca159c1c9fa8a5f9005e42133eed82705bf8e243da371a5e5826440e65ca",
"sha256:b7b20c88873675903d6438d8b33fba027997193e274b9367421e610d9da76c08",
"sha256:bb4b15fb1f0aafa65cbdc62d3c2078bea1ceecbfccc9a1f23a2113c9ac1191fa",
"sha256:c0c7171aa5a57e522a04a31b84798b6c926234cb559c0939840c3235cf068813",
"sha256:c317ddd7c586af350a6aef22b891e84b16bff1a27886ed5b30f15c1ed59caeaa",
"sha256:c3abc34fed19fdeaead0ced8cf56dd121f08198008c033596aa6aae7cc58f59f",
"sha256:ca68c52e3cae491ace2bf39b35fef4ce26c192fd70b4cd90f040d419f70893b5",
"sha256:cf2cd387409b12d0a8b801610d6336ee7d24043b6dd965950eaec09b73e7262f",
"sha256:d046a9aeba9bc53e88a41e58beb72b6205abb9a20f6c136161adf9128e589db5",
"sha256:d5c20c8415173b119762b6110af64448adccd4d11f273fb9f718a9865b88a99c",
"sha256:d86132922531f0dc5a4f424c7580a472a924dd737602638e704841c9cb24aea2",
"sha256:dccff41478050e823271642837b904d5f9bda3f5cf7d371ce163f00a694118d6",
"sha256:de85c26a5a1c72e695ab0454e92f60213b4459b8d7c502e0be7a6369690eeb1a",
"sha256:e3a86b59b6227ef72ffc10d4b23f0fe994bef64d4667eab4fb8cd43de4223bec",
"sha256:e79e73d5ee24196d3057340e356e6254af4d10e1fc22d3207ea8342fc5ffb977",
"sha256:ea8210090a816d48a4291a47462bac750e3bc5c2442e6d64f7b8137a7c3f9ac5",
"sha256:f3b7ec97e68b68cb1f9ddb82eda17b418f19a034fa8380a0ac04e8fe01532875"
],
"index": "pypi",
"version": "==1.4.31"
},
"twitter": {
"hashes": [
"sha256:06eac7ee7f2a14ddeb680671ff07450984f6d254334f5db8dd69547dd1e179c5",
"sha256:a56ff9575fbd50a51ce91107dcb5a4c3fd00c2ba1bcb172ce538b0948d3626e6"
],
"index": "pypi",
"version": "==1.19.3"
},
"urllib3": {
"hashes": [
"sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
"sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.8"
},
"vine": {
"hashes": [
"sha256:4c9dceab6f76ed92105027c49c823800dd33cacce13bdedc5b914e3514b7fb30",
"sha256:7d3b1624a953da82ef63462013bbd271d3eb75751489f9807598e8f340bd637e"
],
"markers": "python_version >= '3.6'",
"version": "==5.0.0"
},
"wcwidth": {
"hashes": [
"sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784",
"sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"
],
"version": "==0.2.5"
},
"werkzeug": {
"hashes": [
"sha256:1421ebfc7648a39a5c58c601b154165d05cf47a3cd0ccb70857cbdacf6c8f2b8",
"sha256:b863f8ff057c522164b6067c9e28b041161b4be5ba4d0daceeaa50a163822d3c"
],
"markers": "python_version >= '3.6'",
"version": "==2.0.3"
},
"wrapt": {
"hashes": [
"sha256:086218a72ec7d986a3eddb7707c8c4526d677c7b35e355875a0fe2918b059179",
"sha256:0877fe981fd76b183711d767500e6b3111378ed2043c145e21816ee589d91096",
"sha256:0a017a667d1f7411816e4bf214646d0ad5b1da2c1ea13dec6c162736ff25a374",
"sha256:0cb23d36ed03bf46b894cfec777eec754146d68429c30431c99ef28482b5c1df",
"sha256:1fea9cd438686e6682271d36f3481a9f3636195578bab9ca3382e2f5f01fc185",
"sha256:220a869982ea9023e163ba915077816ca439489de6d2c09089b219f4e11b6785",
"sha256:25b1b1d5df495d82be1c9d2fad408f7ce5ca8a38085e2da41bb63c914baadff7",
"sha256:2dded5496e8f1592ec27079b28b6ad2a1ef0b9296d270f77b8e4a3a796cf6909",
"sha256:2ebdde19cd3c8cdf8df3fc165bc7827334bc4e353465048b36f7deeae8ee0918",
"sha256:43e69ffe47e3609a6aec0fe723001c60c65305784d964f5007d5b4fb1bc6bf33",
"sha256:46f7f3af321a573fc0c3586612db4decb7eb37172af1bc6173d81f5b66c2e068",
"sha256:47f0a183743e7f71f29e4e21574ad3fa95676136f45b91afcf83f6a050914829",
"sha256:498e6217523111d07cd67e87a791f5e9ee769f9241fcf8a379696e25806965af",
"sha256:4b9c458732450ec42578b5642ac53e312092acf8c0bfce140ada5ca1ac556f79",
"sha256:51799ca950cfee9396a87f4a1240622ac38973b6df5ef7a41e7f0b98797099ce",
"sha256:5601f44a0f38fed36cc07db004f0eedeaadbdcec90e4e90509480e7e6060a5bc",
"sha256:5f223101f21cfd41deec8ce3889dc59f88a59b409db028c469c9b20cfeefbe36",
"sha256:610f5f83dd1e0ad40254c306f4764fcdc846641f120c3cf424ff57a19d5f7ade",
"sha256:6a03d9917aee887690aa3f1747ce634e610f6db6f6b332b35c2dd89412912bca",
"sha256:705e2af1f7be4707e49ced9153f8d72131090e52be9278b5dbb1498c749a1e32",
"sha256:766b32c762e07e26f50d8a3468e3b4228b3736c805018e4b0ec8cc01ecd88125",
"sha256:77416e6b17926d953b5c666a3cb718d5945df63ecf922af0ee576206d7033b5e",
"sha256:778fd096ee96890c10ce96187c76b3e99b2da44e08c9e24d5652f356873f6709",
"sha256:78dea98c81915bbf510eb6a3c9c24915e4660302937b9ae05a0947164248020f",
"sha256:7dd215e4e8514004c8d810a73e342c536547038fb130205ec4bba9f5de35d45b",
"sha256:7dde79d007cd6dfa65afe404766057c2409316135cb892be4b1c768e3f3a11cb",
"sha256:81bd7c90d28a4b2e1df135bfbd7c23aee3050078ca6441bead44c42483f9ebfb",
"sha256:85148f4225287b6a0665eef08a178c15097366d46b210574a658c1ff5b377489",
"sha256:865c0b50003616f05858b22174c40ffc27a38e67359fa1495605f96125f76640",
"sha256:87883690cae293541e08ba2da22cacaae0a092e0ed56bbba8d018cc486fbafbb",
"sha256:8aab36778fa9bba1a8f06a4919556f9f8c7b33102bd71b3ab307bb3fecb21851",
"sha256:8c73c1a2ec7c98d7eaded149f6d225a692caa1bd7b2401a14125446e9e90410d",
"sha256:936503cb0a6ed28dbfa87e8fcd0a56458822144e9d11a49ccee6d9a8adb2ac44",
"sha256:944b180f61f5e36c0634d3202ba8509b986b5fbaf57db3e94df11abee244ba13",
"sha256:96b81ae75591a795d8c90edc0bfaab44d3d41ffc1aae4d994c5aa21d9b8e19a2",
"sha256:981da26722bebb9247a0601e2922cedf8bb7a600e89c852d063313102de6f2cb",
"sha256:ae9de71eb60940e58207f8e71fe113c639da42adb02fb2bcbcaccc1ccecd092b",
"sha256:b73d4b78807bd299b38e4598b8e7bd34ed55d480160d2e7fdaabd9931afa65f9",
"sha256:d4a5f6146cfa5c7ba0134249665acd322a70d1ea61732723c7d3e8cc0fa80755",
"sha256:dd91006848eb55af2159375134d724032a2d1d13bcc6f81cd8d3ed9f2b8e846c",
"sha256:e05e60ff3b2b0342153be4d1b597bbcfd8330890056b9619f4ad6b8d5c96a81a",
"sha256:e6906d6f48437dfd80464f7d7af1740eadc572b9f7a4301e7dd3d65db285cacf",
"sha256:e92d0d4fa68ea0c02d39f1e2f9cb5bc4b4a71e8c442207433d8db47ee79d7aa3",
"sha256:e94b7d9deaa4cc7bac9198a58a7240aaf87fe56c6277ee25fa5b3aa1edebd229",
"sha256:ea3e746e29d4000cd98d572f3ee2a6050a4f784bb536f4ac1f035987fc1ed83e",
"sha256:ec7e20258ecc5174029a0f391e1b948bf2906cd64c198a9b8b281b811cbc04de",
"sha256:ec9465dd69d5657b5d2fa6133b3e1e989ae27d29471a672416fd729b429eb554",
"sha256:f122ccd12fdc69628786d0c947bdd9cb2733be8f800d88b5a37c57f1f1d73c10",
"sha256:f99c0489258086308aad4ae57da9e8ecf9e1f3f30fa35d5e170b4d4896554d80",
"sha256:f9c51d9af9abb899bd34ace878fbec8bf357b3194a10c4e8e0a25512826ef056",
"sha256:fd76c47f20984b43d93de9a82011bb6e5f8325df6c9ed4d8310029a55fa361ea"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.13.3"
}
},
"develop": {
"attrs": {
"hashes": [
"sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4",
"sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==21.4.0"
},
"certifi": {
"hashes": [
"sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872",
"sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"
],
"version": "==2021.10.8"
},
"charset-normalizer": {
"hashes": [
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_version >= '3'",
"version": "==2.0.12"
},
"codecov": {
"hashes": [
"sha256:585dc217dc3d8185198ceb402f85d5cb5dbfa0c5f350a5abcdf9e347776a5b47",
"sha256:782a8e5352f22593cbc5427a35320b99490eb24d9dcfa2155fd99d2b75cfb635",
"sha256:a0da46bb5025426da895af90938def8ee12d37fcbcbbbc15b6dc64cf7ebc51c1"
],
"index": "pypi",
"version": "==2.1.12"
},
"coverage": {
"hashes": [
"sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9",
"sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d",
"sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf",
"sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7",
"sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6",
"sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4",
"sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059",
"sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39",
"sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536",
"sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac",
"sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c",
"sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903",
"sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d",
"sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05",
"sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684",
"sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1",
"sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f",
"sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7",
"sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca",
"sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad",
"sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca",
"sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d",
"sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92",
"sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4",
"sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf",
"sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6",
"sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1",
"sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4",
"sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359",
"sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3",
"sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620",
"sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512",
"sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69",
"sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2",
"sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518",
"sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0",
"sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa",
"sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4",
"sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e",
"sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1",
"sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"
],
"index": "pypi",
"version": "==6.3.2"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_version >= '3'",
"version": "==3.3"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
],
"version": "==1.1.1"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
},
"pluggy": {
"hashes": [
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
],
"markers": "python_version >= '3.6'",
"version": "==1.0.0"
},
"py": {
"hashes": [
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
"sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.11.0"
},
"pyparsing": {
"hashes": [
"sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea",
"sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"
],
"markers": "python_version >= '3.6'",
"version": "==3.0.7"
},
"pytest": {
"hashes": [
"sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db",
"sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"
],
"index": "pypi",
"version": "==7.0.1"
},
"pytest-cov": {
"hashes": [
"sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6",
"sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"
],
"index": "pypi",
"version": "==3.0.0"
},
"requests": {
"hashes": [
"sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61",
"sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"
],
"index": "pypi",
"version": "==2.27.1"
},
"tomli": {
"hashes": [
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.1"
},
"urllib3": {
"hashes": [
"sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed",
"sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.8"
},
"versioneer": {
"hashes": [
"sha256:1b4900f17b84ce76dbc5d462fe06522ea2ec400945c46dc751abad12db2e7ca6",
"sha256:64f2dbcbbed15f9a6da2b85f643997db729cf496cafdb97670fb2fa73a7d8e20"
],
"index": "pypi",
"version": "==0.21"
}
}
}

View File

@ -1,2 +1,3 @@
web: gunicorn -w 9 -t 3600 -b 127.0.0.1:42157 forget:app
worker: python tasks.py -B -Ofair
worker: celery -A tasks worker --autoscale=64,8
beat: celery -A tasks beat

3
Procfile.dev Normal file
View File

@ -0,0 +1,3 @@
web: gunicorn -b 127.0.0.1:5000 --reload --reload-extra-file templates/ -w 4 forget:app
worker: python tasks.py -B -Ofair
build: doit auto

View File

@ -1,38 +1,150 @@
uhh frick i forgot to write a readme hang on uhh
![Forget](assets/promo.gif)
# forget
![Maintenance status](https://img.shields.io/maintenance/no/2022.svg)
its a thing that deletes your posts
[![Build status](https://img.shields.io/travis/codl/forget.svg)](https://travis-ci.org/codl/forget/)
[![Test coverage](https://img.shields.io/codecov/c/github/codl/forget.svg)](https://codecov.io/gh/codl/forget)
it works with twitter and maybe sometime in the future it will work with other services
Forget is a post deleting service for Twitter, Mastodon, and Misskey.
it lives at <https://forget.codl.fr>
you can run your own if you want to, youll need postgresql and redis and python 3.6+
## Features
* 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 or a reaction.
* Set it and <i>forget</i> it. Forget works continuously in the background.
## Non-features
Forget is not a one-time purging tool. It is designed to prune your account
continuously, not quickly. If you need a lot of posts gone fast, you may want
to look for another more-suited tool.
## Running your own
### Requirements
* Postgresql
* Redis
* Python 3.6+
* Node.js 10+
### Set up venv
Setting up a venv will isolate Forget from your system's libraries and allow you to install
dependencies locally as a normal user. It's not necessary but it is recommended!
```
$ # set up virtualenv (recommended)
$ virtualenv venv
$ python -m venv venv
$ source venv/bin/activate
```
$ # install requirements and set up config file
If you're using `zsh` or `fish` as a shell, substitute `venv/bin/activate` with
`venv/bin/activate.zsh` or `venv/bin/activate.fish`, respectively.
You will need to "activate" the venv in every new terminal before you can use
pip or any python tools included in dependencies (honcho, flask...)
### Download and install dependencies
```
$ pip install -r requirements.txt
$ npm install
```
Wow!! Exciting
### Create and complete config file
Gotta set up those, paths, and stuff.
```
$ cp config.example.py config.py
$ $EDITOR config.py
```
$ # set up database schema
$ createdb forget
### Set up database schema
If you haven't started postgresql yet now would be a great time to do that.
```
$ createdb forget # if you havent created the DB yet
$ env FLASK_APP=forget.py flask db upgrade
```
$ # build assets
### Build static assets
Gonna do it...!
```
$ doit
```
$ # start web server and background worker
Done did it.
### Running
The included `Procfile` will run the app server and the background worker.
`honcho`, a `Procfile` runner, is included as a dependency:
```
$ honcho start
```
the web server will listen on `127.0.0.1:42157`, you'll probably want to proxy with nginx or apache or what have you
The application server will listen on `http://127.0.0.1:42157`.
You'll want to use your favourite web server to proxy traffic to it.
sorry this readme sucks i forgot to write one before release
### Development
send me a tweet [@codl](https://twitter.com/codl) if you're having trouble or, to tell me you like it
For development, you may want to use `Procfile.dev`, which starts flask in
debug mode and rebuilds the static assets automatically when they change
```
$ honcho -f Procfile.dev start
```
Or you could just look at `Procfile.dev` and run those things manually. It's up
to you.
You can run the (currently very incomplete) test suite by running `pytest`.
You'll need redis installed on your development machine, a temporary redis
server will be started and shut down automatically by the test suite.
## Docker
This project is also able to be deployed through Docker.
1. Copy `config.docker.py` to `config.py` and add additional configurations to
your liking.
1. By default, the webapp container will be listening on `127.0.0.1:42157`,
which you can point a reverse proxy at.
* If your reverse proxy is in another docker network then you'll need a
`docker-compose.override.yml` file to attach the `www` service to the
right network and not publish any ports. An example override file is
provided. The web app will be listening on `http://forget-www-1:42157`.
1. By default, the `docker-compose.yml` creates relative mounts `./redis`,
`./postgres`, and `./celery` relative to the `docker-compose.yml` location.
An example `docker-compose.override.yml` file is provided that shows how to
change this.
1. Run `docker-compose build` to build the image.
1. Run `docker-compose up` to start or `docker-compose up -d` to start in the
background, and use `docker-compose down` to stop.
## Contact
If you're having trouble with Forget, or if you're not having trouble but you
just want to tell me you like it, you can drop me a note at
[@codl@chitter.xyz](https://chitter.xyz/@codl) or
[codl@codl.fr](mailto:codl@codl.fr).
## Greetz
Thank you bea, for making ephemeral, inspiring me to make [limiter][], then this,
in an attempt to bring ephemeral with me everywhere. ☕
[limiter]: https://github.com/codl/limiter
Thank you to the kind folks who have emailed me to tell me Forget has made their
time on social media less stressful. 🌻

77
app.py
View File

@ -1,30 +1,31 @@
from flask import Flask, request
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import MetaData
from flask_migrate import Migrate
import version
from lib import cachebust
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
from lib import get_viewer
from libforget.cachebust import cachebust
import mimetypes
import libforget.brotli
import libforget.img_proxy
from werkzeug.middleware.proxy_fix import ProxyFix
app = Flask(__name__)
default_config = {
"SQLALCHEMY_TRACK_MODIFICATIONS": False,
"SQLALCHEMY_DATABASE_URI": "postgresql+psycopg2:///forget",
"SECRET_KEY": "hunter2",
"CELERY_BROKER": "redis://",
"HTTPS": True,
"SENTRY_CONFIG": {},
"RATELIMIT_STORAGE_URL": "redis://",
"REPO_URL": "https://github.com/codl/forget",
"CHANGELOG_URL": "https://github.com/codl/forget/blob/{hash}/CHANGELOG.markdown",
"REDIS_URI": "redis://",
}
app.config.update(default_config)
app.config.from_pyfile('config.py', True)
metadata = MetaData(naming_convention = {
metadata = MetaData(naming_convention={
"ix": 'ix_%(column_0_label)s',
"uq": "uq_%(table_name)s_%(column_0_name)s",
"ck": "ck_%(table_name)s_%(constraint_name)s",
@ -35,28 +36,64 @@ metadata = MetaData(naming_convention = {
db = SQLAlchemy(app, metadata=metadata)
migrate = Migrate(app, db)
if 'CELERY_BROKER' not in app.config:
uri = app.config['REDIS_URI']
if uri.startswith('unix://'):
uri = uri.replace('unix', 'redis+socket', 1)
app.config['CELERY_BROKER'] = uri
sentry = None
if 'SENTRY_DSN' in app.config:
from raven.contrib.flask import Sentry
app.config['SENTRY_CONFIG']['release'] = version.version
app.config['SENTRY_CONFIG']['release'] = version.get_versions()['version']
sentry = Sentry(app, dsn=app.config['SENTRY_DSN'])
url_for = cachebust(app)
@app.context_processor
def inject_static():
def static(filename, **kwargs):
return url_for('static', filename=filename, **kwargs)
return {'st': static}
def rate_limit_key():
viewer = get_viewer()
if viewer:
return viewer.id
for address in request.access_route:
if address != '127.0.0.1':
print(address)
return address
return request.remote_addr
limiter = Limiter(app, key_func=rate_limit_key)
@app.after_request
def install_security_headers(resp):
csp = ("default-src 'none';"
"img-src 'self';"
"style-src 'self' 'unsafe-inline';"
"frame-ancestors 'none';"
)
if 'SENTRY_DSN' in app.config:
csp += "script-src 'self' https://cdn.ravenjs.com/;"
csp += "connect-src 'self' https://sentry.io/;"
else:
csp += "script-src 'self' 'unsafe-eval';"
csp += "connect-src 'self';"
if 'CSP_REPORT_URI' in app.config:
csp += "report-uri " + app.config.get('CSP_REPORT_URI')
if app.config.get('HTTPS'):
resp.headers.set('strict-transport-security',
'max-age={}'.format(60*60*24*365))
csp += "; upgrade-insecure-requests"
resp.headers.set('Content-Security-Policy', csp)
resp.headers.set('referrer-policy', 'no-referrer')
resp.headers.set('x-content-type-options', 'nosniff')
resp.headers.set('x-frame-options', 'DENY')
resp.headers.set('x-xss-protection', '1')
return resp
mimetypes.add_type('image/webp', '.webp')
libforget.brotli.brotli(app)
imgproxy = (
libforget.img_proxy.ImgProxyCache(redis_uri=app.config.get('REDIS_URI')))
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@ -16,7 +16,7 @@
id="svg8"
inkscape:version="0.92.1 r"
sodipodi:docname="icon.svg"
inkscape:export-filename="/home/codl/dev/forget/static/icon.png"
inkscape:export-filename="/home/codl/dev/forget/assets/icon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
@ -26,11 +26,11 @@
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:zoom="2.8567114"
inkscape:cx="34.405784"
inkscape:cy="62.165837"
inkscape:zoom="1.4283557"
inkscape:cx="184.99523"
inkscape:cy="-92.562755"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
@ -43,7 +43,8 @@
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="1" />
inkscape:window-maximized="1"
inkscape:pagecheckerboard="true" />
<metadata
id="metadata5">
<rdf:RDF>
@ -61,13 +62,13 @@
inkscape:groupmode="layer"
id="layer1"
transform="translate(84.097282,-171.40979)">
<rect
style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.26458332;stroke-linecap:butt;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect4489"
width="37.139881"
height="37.139881"
x="-84.097282"
y="171.40979" />
<path
transform="matrix(0.26458333,0,0,0.26458333,-84.097282,171.40979)"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 32.939453,3.1972656 C 24.168569,3.0451495 15.079238,7.6549961 11.033203,16.685547 c -8.4731529,18.911677 0.08466,41.03752 7.3125,60.478515 3.631125,9.766774 7.312103,19.040448 9.494141,26.453128 1.07333,3.64625 1.715203,6.97656 1.914062,9.47461 0.780434,3.12173 0.687876,5.12719 -0.767152,6.9847 l 10.287032,18.50112 c 1.781562,-0.48657 1.102772,-0.30657 2.903948,-1.09715 1.500393,-0.6583 2.781027,-0.97068 5.667969,-3.40235 6.953155,-5.85528 8.501804,-14.76179 8.296875,-22.9746 -0.106144,-4.25391 -0.825456,-8.66099 -1.697266,-13.099614 3.56439,-0.364764 7.038375,-1.236687 10.357422,-2.53125 0.595555,6.809914 3.787703,12.887944 8.369141,17.191404 5.951744,5.59062 13.747643,8.65249 21.671875,9.53711 7.93258,0.88555 16.43751,-0.39549 23.59375,-5.08789 7.29046,-4.78041 12.4389,-13.30017 12.90039,-23.757811 0.43456,-9.847238 -2.68108,-18.233863 -8.64844,-23.457031 -5.8482,-5.11886 -13.11537,-6.604703 -19.62304,-6.617188 -4.002224,-0.0077 -8.038878,0.399729 -11.974613,1.019531 1.060396,-6.714583 0.246984,-13.93875 -3.353516,-20.873047 C 82.118522,32.604501 73.360599,23.04592 63.75,15.826172 54.172795,8.6315103 43.653188,3.3830772 32.939453,3.1972656 Z"
id="path4571"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssscccccsccsssssccsss" />
<path
style="fill:#000000;fill-rule:nonzero;stroke:#0b0b0b;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="m -74.470561,193.73092 c 5.501238,4.06044 14.931932,-2.8816 11.395421,-9.69266 -3.53651,-6.81105 -13.448034,-12.5624 -15.848803,-7.204 -3.258985,7.2739 6.922683,21.59014 4.97731,26.45834 l 0.916874,1.70276 c 4.97731,-4.19142 -5.89899,-23.53379 -3.274548,-26.45834 2.591897,-2.88827 10.511924,4.25244 11.264439,7.59695 1.178837,5.23928 -5.108291,4.97731 -9.430693,7.59695 z"

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,74 @@
import {SLOTS, normalize_known, known_load, known_save} from './known_instances.js';
(function instance_buttons(){
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('#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 replace_buttons(){
let known = known_load();
known = normalize_known(known);
known_save(known);
let filtered_top_instances = []
for(let instance of top_instances){
let found = false;
for(let k of known_instances){
if(k['instance'] == instance['instance']){
found = true;
break;
}
}
if(!found){
filtered_top_instances.push(instance)
}
}
let instances = known_instances.concat(filtered_top_instances).slice(0, SLOTS);
let html = '';
let first = true;
for(let instance of instances){
html += template(first, instance['instance'])
first = false;
}
html += template_another_instance();
container.innerHTML = html;
}
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();
})();

72
assets/known_instances.js Normal file
View File

@ -0,0 +1,72 @@
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));
}
export function known_load(){
const default_ = {
mastodon:[{ "instance": "mastodon.social", "hits": 0 }],
misskey:[{ "instance": "misskey.io", "hits": 0 }],
};
// this makes mastodon.social and misskey.io show up on respective first
// buttons by default even if they are not the most popular instance
// according to the server
let known = localStorage.getItem(STORAGE_KEY);
if(known){
known = JSON.parse(known);
} else {
known = load_and_migrate_old();
}
return known || default_;
}
export function normalize_known(known){
/*
move instances with the most hits to the top SLOTS slots,
making sure not to reorder anything that is already there
*/
let head = known.slice(0, SLOTS);
let tail = known.slice(SLOTS);
if(tail.length == 0){
return known;
}
for(let i = 0; i < SLOTS; i++){
let head_min = head.reduce((acc, cur) => acc.hits < cur.hits ? acc : cur);
let tail_max = tail.reduce((acc, cur) => acc.hits > cur.hits ? acc : cur);
if(head_min.hits < tail_max.hits){
// swappy
let i = head.indexOf(head_min);
let j = tail.indexOf(tail_max);
let buf = head[i];
head[i] = tail[j];
tail[j] = buf;
}
}
return head.concat(tail)
}

BIN
assets/mastodon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

61
assets/mastodon.svg Normal file
View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 101.25 101.24999"
height="108"
width="108"
version="1.1"
id="svg4"
sodipodi:docname="mastodon.svg"
inkscape:version="0.92.1 r"
inkscape:export-filename="/home/codl/dev/forget/assets/mastodon.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<metadata
id="metadata10">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs8" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1219"
inkscape:window-height="900"
id="namedview6"
showgrid="false"
inkscape:zoom="2.194861"
inkscape:cx="50.601219"
inkscape:cy="53.761946"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="0"
inkscape:current-layer="svg4"
units="px"
inkscape:pagecheckerboard="true" />
<path
d="m 76.184258,49.229421 c -3.9125,0 -7.085,-3.1825 -7.085,-7.095 0,-3.91125 3.1725,-7.09375 7.085,-7.09375 3.92125,0 7.09375,3.1825 7.09375,7.09375 0,3.9125 -3.1725,7.095 -7.09375,7.095 m -25.55875,0 c -3.9225,0 -7.095,-3.1825 -7.095,-7.095 0,-3.91125 3.1725,-7.09375 7.095,-7.09375 3.91125,0 7.09375,3.1825 7.09375,7.09375 0,3.9125 -3.1825,7.095 -7.09375,7.095 m -25.57,0 c -3.91125,0 -7.08375,-3.1825 -7.08375,-7.095 0,-3.91125 3.1725,-7.09375 7.08375,-7.09375 3.92125,0 7.09375,3.1825 7.09375,7.09375 0,3.9125 -3.1725,7.095 -7.09375,7.095 m 72.5775,-15.905 c 0,-21.86625 -14.32375,-28.2737494 -14.32375,-28.2737494 -7.23,-3.31875 -19.63,-4.71250004 -32.5175,-4.82750004 h -0.3125 c -12.88875,0.115 -25.28875,1.50875004 -32.5075,4.82750004 0,0 -14.3237495,6.4074994 -14.3237495,28.2737494 0,5.00375 -0.105,10.995 0.05125,17.34 0.52,21.38875 3.92125,42.46375 23.6974995,47.69625 9.1125,2.412499 16.945,2.912499 23.24875,2.568749 11.4225,-0.63375 17.84,-4.076249 17.84,-4.076249 l -0.37375,-8.3025 c 0,0 -8.16625,2.58 -17.34125,2.2675 -9.09125,-0.3125 -18.6825,-0.9775 -20.16,-12.13875 -0.135,-0.97875 -0.1975,-2.02875 -0.1975,-3.13125 0,0 8.915,2.185 20.2325,2.69375 6.9075,0.3225 13.39875,-0.39375 19.98375,-1.185 12.6275,-1.50875 23.62375,-9.29 25.0075,-16.405 2.17375,-11.1925 1.99625,-27.3275 1.99625,-27.3275"
id="path2"
style="fill:#ffffff"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
assets/misskey.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
assets/promo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

224
assets/settings.js Normal file
View File

@ -0,0 +1,224 @@
import Banner from '../components/Banner.html';
import {known_load, known_save} from './known_instances.js'
(function settings_init(){
if(!('fetch' in window)){
return;
}
let status_timeout = null;
let settings_section = document.querySelector('#settings-section');
let form = document.querySelector('form[name=settings]');
let inputs = Array.from(form.elements)
let backoff_level = 0;
let banner_el = document.querySelector('.main-banner');
banner_el.innerHTML = '';
let banner = new Banner({
target: banner_el,
});
function hide_status(){
status_display.classList.remove('error', 'success', 'saving');
status_display.classList.add('hidden');
status_display.innerHTML='';
}
function show_error(){
hide_status();
status_display.textContent='Could not save. Retrying...';
status_display.classList.add('error');
status_display.classList.remove('hidden');
}
function show_success(){
hide_status();
status_display.textContent='Saved!';
status_display.classList.add('success');
status_display.classList.remove('hidden');
}
function show_still_saving(){
status_display.textContent='Still saving...';
}
function show_saving(){
hide_status();
status_display.textContent='Saving...';
status_display.classList.add('saving');
status_display.classList.remove('hidden');
status_timeout = setTimeout(show_still_saving, 5000);
}
function save(){
hide_status();
clearTimeout(status_timeout);
status_timeout = setTimeout(show_saving, 70);
let promise = send_settings(get_all_inputs())
.then(() => {
show_success();
clearTimeout(status_timeout);
status_timeout = setTimeout(hide_status, 3000);
backoff_level = 0;
});
promise.catch(() => {
show_error();
clearTimeout(status_timeout);
status_timeout = setTimeout(save, Math.pow(2, backoff_level)*1000);
backoff_level += 1;
backoff_level = Math.min(backoff_level, 5);
});
promise.then(fetch_viewer).then(update_viewer);
// remove server-rendered banner
let banner = settings_section.querySelector('.banner');
if(banner){
settings_section.removeChild(banner);
}
}
function get_all_inputs(){
let o = Object();
for(let input of inputs){
if(input.type != 'radio' || input.checked){
o[input.name] = input.value;
}
}
return o;
}
function send_settings(body){
return fetch('/api/settings', {
method:'PUT',
credentials:'same-origin',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body)
})
.then(resp => {
if(!resp.ok){ return Promise.reject(resp); }
return resp; })
.then(resp => resp.json())
.then(data => {
if(data.status == 'error'){ return Promise.reject(data); }
return data;
});
}
for(let input of inputs){
input.addEventListener('change', save);
}
// remove submit button since we're doing live updates
let submit = form.querySelector('input[type=submit]');
form.removeChild(submit);
inputs.splice(inputs.indexOf(submit), 1);
let status_display = document.createElement('span');
status_display.classList.add('status-display', 'hidden');
settings_section.insertBefore(status_display, settings_section.childNodes[0]);
// silently send_settings in case the user changed settings while the page was loading
send_settings(get_all_inputs());
let viewer_update_interval = 1500;
function fetch_viewer(){
viewer_update_interval *= 2;
viewer_update_interval = Math.min(30000, viewer_update_interval);
return fetch('/api/viewer', {
credentials: 'same-origin',
})
.then(resp => {
if(!resp.ok){
if(resp.status == 403){
// user was logged out in another client
window.location = '/';
}
return Promise.reject(resp);
}
return resp; })
.then(resp => resp.json());
}
let last_viewer = {};
function update_viewer(viewer){
let dumped = JSON.stringify(viewer);
if(last_viewer == dumped){
return;
}
last_viewer = dumped;
document.querySelector('#post-count').textContent = viewer.post_count;
document.querySelector('#eligible-estimate').textContent = viewer.eligible_for_delete_estimate;
document.querySelector('#display-name').textContent = viewer.display_name || viewer.screen_name;
document.querySelector('#display-name').title = '@' + viewer.screen_name;
document.querySelector('#avatar').src = viewer.avatar_url;
viewer_update_interval = 1500;
if(viewer.next_delete){
viewer.next_delete = new Date(viewer.next_delete);
}
if(viewer.last_delete){
viewer.last_delete = new Date(viewer.last_delete);
}
banner.$set(viewer);
}
let viewer_from_dom = JSON.parse(document.querySelector('script[data-viewer]').textContent)
update_viewer(viewer_from_dom)
function set_viewer_timeout(){
setTimeout(() => fetch_viewer().then(update_viewer).then(set_viewer_timeout, set_viewer_timeout),
viewer_update_interval);
}
set_viewer_timeout();
banner.$on('toggle', event => {
let enabled = event.detail;
send_settings({policy_enabled: enabled}).then(fetch_viewer).then(update_viewer);
// TODO show error or spinner if it takes over a second
})
let reason_banner = document.querySelector('.banner[data-reason]');
if(reason_banner){
let dismiss = reason_banner.querySelector('input[type=submit]');
dismiss.addEventListener('click', e => {
e.preventDefault();
// we don't care if this succeeds or fails. worst
// case scenario the banner appears again on a future page load
fetch('/api/reason', {method: 'DELETE', credentials:'same-origin'});
reason_banner.parentElement.removeChild(reason_banner);
})
}
function bump_instance(service, instance_name){
let known_instances = known_load();
let found = false;
for(let instance of known_instances[service]){
if(instance['instance'] == instance_name){
instance.hits ++;
found = true;
break;
}
}
if(!found){
let instance = {"instance": instance_name, "hits": 1};
known_instances[service].push(instance);
}
known_save(known_instances);
}
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);
}
})();

View File

@ -1,111 +0,0 @@
(function(){
if(!('fetch' in window)){
return;
}
let status_timeout = null;
let form = document.forms.settings;
let backoff_level = 0
function hide_status(){
status_display.classList.remove('error', 'success', 'saving');
status_display.classList.add('hidden');
status_display.innerHTML='';
}
function show_error(e){
hide_status();
status_display.textContent='Could not save. Retrying...';
status_display.classList.add('error');
status_display.classList.remove('hidden');
}
function show_success(){
hide_status();
status_display.textContent='Saved!';
status_display.classList.add('success');
status_display.classList.remove('hidden');
}
function show_saving(){
hide_status();
status_display.textContent='Saving...';
status_display.classList.add('saving');
status_display.classList.remove('hidden');
status_timeout = setTimeout(show_still_saving, 5000);
}
function show_still_saving(){
status_display.textContent='Still saving...';
}
function on_change(e){
hide_status();
clearTimeout(status_timeout);
status_timeout = setTimeout(show_saving, 70);
send_settings(get_all_inputs())
.then(data => {
show_success();
clearTimeout(status_timeout);
status_timeout = setTimeout(hide_status, 3000);
backoff_level = 0;
})
.catch(e => {
console.error('Fetch rejected:', e);
show_error();
clearTimeout(status_timeout);
status_timeout = setTimeout(save, Math.pow(2, backoff_level)*1000);
backoff_level += 1;
backoff_level = Math.min(backoff_level, 5);
});
// remove server-rendered banner
let settings_section = document.querySelector('#settings-section');
let banner = settings_section.querySelector('.banner');
if(banner){
settings_section.removeChild(banner);
}
}
function get_all_inputs(){
let o = Object()
for(input of form.elements){
if(input.type != 'radio' || input.checked){
o[input.name] = input.value;
}
}
return o
}
function send_settings(body){
return fetch('/api/settings', {
method:'PUT',
credentials:'same-origin',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(body)
})
.then(resp => { if(!resp.ok){ return Promise.reject(resp) }; return resp; })
.then(resp => resp.json())
.then(data => {
if(data.status == 'error'){ return Promise.reject(data) }
return data
});
}
for(input of form.elements){
input.addEventListener('change', on_change);
input.addEventListener('change', e=>console.log(e.target));
}
// remove submit button since we're doing live updates
let submit = form.querySelector('input[type=submit]');
form.removeChild(submit);
let status_display = document.createElement('span');
status_display.classList.add('status-display', 'hidden');
let settings_title = document.querySelector('#settings-title');
settings_title.appendChild(status_display);
// silently send_settings in case the user changed settings while the page was loading
send_settings(get_all_inputs());
})();

View File

@ -1,15 +1,15 @@
body {
margin: 0;
font-family: sans-serif;
line-height: 1.5em;
}
*, *::before, *::after {
box-sizing: border-box;
line-height: 1.5em;
}
body > section, body > header, body > footer {
max-width: 40rem;
max-width: 45rem;
margin-left: auto;
margin-right: auto;
}
@ -32,6 +32,10 @@ section > * {
padding-right: 5rem;
}
section > .container {
padding: 0;
}
section > ul {
padding-left: 7rem;
}
@ -40,19 +44,21 @@ h2 {
font-size: 1.4em;
font-weight: normal;
padding-left: 2rem;
padding-right: 2rem;
}
h3 {
font-size: inherit;
font-weight: bold;
padding-left: 5rem;
padding-right: 5rem;
}
input[type=number]{
max-width: 8ch;
}
.viewer img.avatar {
img.avatar {
height: 1.5em;
width: 1.5em;
background-color: #ccc;
@ -60,21 +66,37 @@ input[type=number]{
transform:translateY(25%);
}
img.avatar.mastodon {
border-radius: 20%;
}
.banner {
border-left-width: .7rem;
border-left-style: solid;
padding-top: .6rem;
padding-bottom: .6rem;
margin-bottom: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
body > section > .banner {
.banner p {
margin: .3rem 0;
}
.banner p ~ p {
margin-top: 1.3rem;
}
body > section > .banner,
body > section > .container > .banner {
padding-left: 4.3rem;
padding-right: 4.3rem;
}
.banner.enabled {
border-left-color: transparent;
background: #bbfbff;
background: #cde;
}
.banner.disabled {
@ -113,13 +135,7 @@ footer {
font-size: 0.9rem;
margin-top: 5rem;
margin-bottom: 3rem;
display: flex;
flex-direction: row;
justify-content: center;
}
footer p {
margin: 0 0.7rem;
text-align: center;
}
footer a {
@ -132,6 +148,9 @@ footer a {
margin-left: 1ch;
padding: 0 0.4em;
vertical-align: top;
position: sticky;
top: 0;
float: right;
}
.status-display.hidden {
@ -143,7 +162,6 @@ footer a {
}
.status-display.success {
display: inline-block;
background: #dec;
animation: fade-background 2s forwards;
}
@ -178,5 +196,146 @@ footer a {
}
}
form aside {
font-style: italic;
font-size: 0.8em;
}
button {
font-size: inherit;
}
.btn {
text-decoration: none;
border-radius: .2rem;
overflow: hidden;
display: inline-block;
padding: 0.4em 0.8em;
border: none;
cursor: pointer;
}
.btn-small {
font-size: 0.8em;
padding: 0.2em 0.5em;
}
.btn.primary {
color: white;
background-color: #37d;
}
.btn.primary.twitter-colored {
background-color: #1da1f2;
}
.btn.primary.mastodon-colored {
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);
color: inherit;
}
.btn:hover {
opacity: 0.9;
}
.btn img {
height: 1.1em;
transform: translatey(.2em);
margin: 0 .3em;
}
.btn img:first-child,
.btn picture:first-child img {
margin-left: 0;
}
.btn-group.inline {
display: inline-block;
transform: translatey(20%);
}
.btn-group.right {
float: right;
margin-left: 1em;
}
form.btn-group {
margin-top: 0; margin-bottom: 0;
}
.clearfix {
clear: both;
}
section > pre.error-log {
overflow: auto;
padding-top: 2em;
padding-bottom: 2em;
opacity: 0.7;
}
.radiostrip {
display: inline-flex;
flex-direction: row;
flex-wrap: wrap;
}
.radiostrip .choice {
position: relative;
}
.radiostrip input[type=radio] {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.radiostrip label {
padding: .3em .5em;
user-select: none;
background-color: transparent;
border: 1px solid rgba(0,0,0,0.3);
}
.radiostrip span.choice:first-child label {
border-radius: .2rem 0 0 .2rem;
}
.radiostrip span.choice:last-child label {
border-radius: 0 .2rem .2rem 0;
}
.radiostrip span.choice:not(:first-child) label {
border-left-color: transparent;
}
.radiostrip span.choice:not(:last-child) label {
border-right-color: transparent;
}
.radiostrip input[type=radio]:focus + label {
box-shadow: 0 0 5px #38f;
}
.radiostrip input[type=radio]:checked + label {
background: #37d;
color: white;
border-color: transparent;
}

BIN
assets/twitter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

63
assets/twitter.svg Normal file
View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 20.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
x="0px"
y="0px"
viewBox="0 0 400 400"
style="enable-background:new 0 0 400 400;"
xml:space="preserve"
id="svg12"
sodipodi:docname="twitter.svg"
inkscape:version="0.92.1 r"
inkscape:export-filename="/home/codl/dev/forget/assets/twitter.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><metadata
id="metadata18"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title /></cc:Work></rdf:RDF></metadata><defs
id="defs16" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview14"
showgrid="false"
inkscape:zoom="0.59"
inkscape:cx="-95.762712"
inkscape:cy="193.22034"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:window-maximized="1"
inkscape:current-layer="svg12"
inkscape:pagecheckerboard="true" /><style
type="text/css"
id="style2">
.st0{opacity:0.15;fill:#292F33;}
.st1{fill:#FFFFFF;}
</style><g
id="_x31_0_x2013_20_x25__Black_Tint" /><g
id="Logo__x2014__FIXED"
transform="matrix(1.5838783,0,0,1.5838783,-116.80485,-116.77566)"
style="stroke-width:0.63136166"><g
id="g9"
style="stroke-width:0.63136166"><path
class="st1"
d="m 153.6,301.6 c 94.3,0 145.9,-78.2 145.9,-145.9 0,-2.2 0,-4.4 -0.1,-6.6 10,-7.2 18.7,-16.3 25.6,-26.6 -9.2,4.1 -19.1,6.8 -29.5,8.1 10.6,-6.3 18.7,-16.4 22.6,-28.4 -9.9,5.9 -20.9,10.1 -32.6,12.4 -9.4,-10 -22.7,-16.2 -37.4,-16.2 -28.3,0 -51.3,23 -51.3,51.3 0,4 0.5,7.9 1.3,11.7 -42.6,-2.1 -80.4,-22.6 -105.7,-53.6 -4.4,7.6 -6.9,16.4 -6.9,25.8 0,17.8 9.1,33.5 22.8,42.7 -8.4,-0.3 -16.3,-2.6 -23.2,-6.4 0,0.2 0,0.4 0,0.7 0,24.8 17.7,45.6 41.1,50.3 -4.3,1.2 -8.8,1.8 -13.5,1.8 -3.3,0 -6.5,-0.3 -9.6,-0.9 6.5,20.4 25.5,35.2 47.9,35.6 -17.6,13.8 -39.7,22 -63.7,22 -4.1,0 -8.2,-0.2 -12.2,-0.7 22.6,14.4 49.6,22.9 78.5,22.9"
id="path7"
inkscape:connector-curvature="0"
style="fill:#ffffff;stroke-width:0.63136166" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -1,14 +0,0 @@
(function(){
if(!('fetch' in window)){
return
}
fetch('/api/about').then(r => r.json()).then(j => {
let ident = document.querySelector('#ident');
ident.textContent = j.service
if(j.version){
ident.textContent += ' ' + j.version
}
})
})();

133
components/Banner.html Normal file
View File

@ -0,0 +1,133 @@
<div class='banner {policy_enabled?'enabled':'disabled'}'>
<div class='btn-group right'>
<button class='btn {policy_enabled?"secondary":"primary"}' on:click={toggle}>{policy_enabled?'Disable':'Enable'}</button>
</div>
<div>
Forget is currently
{#if policy_enabled }
<b>enabled</b>
{:else}
disabled
{/if}
on your account.
</div>
<div class='timers'>
<span class='last-delete' class:hidden={!last_delete} title={last_delete}>
{#if last_delete }
Last delete {rel_past(now - last_delete)}.
{/if }
</span>
<span class='next-delete'
class:hidden={!policy_enabled || !next_delete || !eligible_for_delete_estimate} title={next_delete}>
Next delete {rel_future(next_delete - now)}.
</span>
</div>
</div>
<style>
.timers {
font-size: 0.8em;
}
.timers > * {
transition-property: opacity, transform;
transition-duration: 0.4s;
display: inline-block;
}
.timers > .hidden {
opacity: 0;
transform: translateY(-0.3em);
pointer-events: none;
}
.banner {
transition: background-color 0.6s;
}
</style>
<script>
function absmod(n, x){
// it's like modulo but never negative
n = n % x;
if(n < 0){
n += x
}
return n
}
function s(n){
// utility for plurals
if(n > 1){
return 's';
}
return '';
}
function rel(millis){
// returns human-readable duration from duration in millis
let secs = Math.round(millis/1000)
if(secs <= 120){
return `${secs} seconds`;
}
let mins = Math.round(secs/60);
if(mins <= 60){
return `${mins} minute${s(mins)}`;
}
let hours = Math.floor(mins/60);
mins = mins % 60;
if(hours < 6){
return `${hours}h ${mins}m`;
}
if(hours <= 48){
return `${hours} hour${s(hours)}`;
}
let days = Math.round(hours/24);
return `${days} days`;
}
function rel_future(millis){
// returns relative time from timestamp, assuming time is in the future
if(millis < 2000){
let secs = Math.floor(millis/1000)
let ndots = absmod(-secs, 3);
let out = 'anytime now';
for(; ndots > 0; ndots--){
out += '.';
}
return out;
}
return `in ${rel(millis)}`;
}
function rel_past(millis){
// returns relative time from timestamp, assuming time is in the past
if(millis < 2000){
return 'just now';
}
return `${rel(millis)} ago`;
}
import { onDestroy, createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
function toggle(){
console.log(policy_enabled);
policy_enabled = !policy_enabled;
if(policy_enabled){
next_delete = null;
}
dispatch('toggle', policy_enabled);
}
export let next_delete, last_delete, eligible_for_delete_estimate;
export let policy_enabled = false;
let now = +(new Date());
let interval = setInterval(() =>
now = +(new Date())
, 1000 );
onDestroy(()=> clearInterval(interval));
</script>

76
config.docker.py Normal file
View File

@ -0,0 +1,76 @@
"""
this is an example config file for Forget
copy this file to config.py before editing
lines starting with # demonstrate default or example values
the # should be removed before editing
"""
"""
DATABASE URI
determines where to connect to the database
see <http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls> for syntax
only postgresql with psycopg2 driver is officially supported
"""
SQLALCHEMY_DATABASE_URI='postgresql+psycopg2://postgres:postgres@db/forget'
"""
REDIS URI
see <https://redis-py.readthedocs.io/en/latest/#redis.ConnectionPool.from_url>
for syntax reference
"""
REDIS_URI='redis://redis'
"""
SERVER ADDRESS
This is the address at which forget will be reached.
External services will redirect to this address when logging in.
"""
# SERVER_NAME="0.0.0.0:5000"
# HTTPS=True
"""
TWITTER CREDENTIALS
Apply for api keys on the developer portal <https://developer.twitter.com/en/apps>
When prompted for it, your callback URL is {SERVER_NAME}/login/twitter/callback
"""
# TWITTER_CONSUMER_KEY='yN3DUNVO0Me63IAQdhTfCA'
# TWITTER_CONSUMER_SECRET='c768oTKdzAjIYCmpSNIdZbGaG0t6rOhSFQP0S5uC79g'
"""
SENTRY
If you want to send exceptions to sentry, enter your sentry DSN here
"""
# SENTRY_DSN=''
"""
HIDDEN INSTANCES
The front page shows one-click login buttons for the mastodon and
misskey instances that see the most heavy use. Instances configured in this
list will be prevented from appearing in these buttons.
They will still appear if a user has previously logged into them and their
browser remembers it. A user will still be able to log into them by manually
typing the address into the log in form.
This is a space-delimited list. Example syntax:
HIDDEN_INSTANCES='social.example.com pleroma.example.net mk.example.org'
"""
# HIDDEN_INSTANCES=''
"""
ADVANCED FLASK CONFIG
you can also use any config variable that flask expects here
A list of these config variables is available here:
<http://flask.pocoo.org/docs/1.0/config/#builtin-configuration-values>
"""
# SESSION_COOKIE_SECURE=True
# DEBUG=True

View File

@ -2,59 +2,75 @@
this is an example config file for Forget
copy this file to config.py before editing
lines starting with # demonstrate default or example values
the # should be removed before editing
"""
"""
DATABASE URI
determines where to connect to the database
see http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls for syntax
see <http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls> for syntax
only postgresql with psycopg2 driver is officially supported
"""
# SQLALCHEMY_DATABASE_URI='postgresql+psycopg2:///forget'
"""
TWITTER CREDENTIALS
REDIS URI
get these at apps.twitter.com
blah
see <https://redis-py.readthedocs.io/en/latest/#redis.ConnectionPool.from_url>
for syntax reference
"""
# TWITTER_CONSUMER_KEY='vdsvdsvds'
# TWITTER_CONSUMER_SECRET='hjklhjklhjkl'
# REDIS_URI='redis://'
"""
this will be necessary so we can tell twitter where to redirect
SERVER ADDRESS
This is the address at which forget will be reached.
External services will redirect to this address when logging in.
"""
# SERVER_NAME="localhost:5000"
# CELERY_BROKER='redis://'
# HTTPS=True
# SENTRY_DSN='https://foo:bar@sentry.io/69420'
"""
TWITTER CREDENTIALS
'''
you can set this to memory:// if you only have one web process
or if you don't care about people exhausting your twitter api
key and your celery workers by making hundreds of login
requests and uploading hundreds of bogus tweet archives
docs here <https://flask-limiter.readthedocs.io/en/stable/#configuration>
'''
# RATELIMIT_STORAGE_URL='redis://'
# REDIS=dict(
# db=0
#
# host='localhost'
# port=6379
# # or...
# unix_socket_path='/var/run/redis/redis.sock'
# # see `pydoc redis.StrictRedis.__init__` for full list of arguments
# )
Apply for api keys on the developer portal <https://developer.twitter.com/en/apps>
When prompted for it, your callback URL is {SERVER_NAME}/login/twitter/callback
"""
# TWITTER_CONSUMER_KEY='yN3DUNVO0Me63IAQdhTfCA'
# TWITTER_CONSUMER_SECRET='c768oTKdzAjIYCmpSNIdZbGaG0t6rOhSFQP0S5uC79g'
"""
you can also use any config variable that flask expects here, such as
SENTRY
If you want to send exceptions to sentry, enter your sentry DSN here
"""
# SENTRY_DSN=''
"""
HIDDEN INSTANCES
The front page shows one-click login buttons for the mastodon and
misskey instances that see the most heavy use. Instances configured in this
list will be prevented from appearing in these buttons.
They will still appear if a user has previously logged into them and their
browser remembers it. A user will still be able to log into them by manually
typing the address into the log in form.
This is a space-delimited list. Example syntax:
HIDDEN_INSTANCES='social.example.com pleroma.example.net mk.example.org'
"""
# HIDDEN_INSTANCES=''
"""
ADVANCED FLASK CONFIG
you can also use any config variable that flask expects here
A list of these config variables is available here:
<http://flask.pocoo.org/docs/1.0/config/#builtin-configuration-values>
"""
# SESSION_COOKIE_SECURE=True
# DEBUG=True

0
data/.keep Normal file
View File

View File

@ -0,0 +1,33 @@
## uncomment below and change network name to use a reverse proxy on an
## existing network
#networks:
# mycoolreverseproxynetwork:
# external: true
services:
www:
## uncomment below to stop listening on 127.0.0.1
# ports: []
networks:
- forget
## uncomment below and change network name to use a reverse proxy on an
## existing network
# - mycoolreverseproxynetwork
## if you wish to change where postgres, redis, and celery persistent data is
## stored, uncomment below and change the first part of each volume (before the `:`)
# redis:
# volumes:
# - /my/cool/redis/path:/data
# db:
# volumes:
# - /my/cool/postgres/path:/var/lib/postgresql/data
# worker:
# volumes:
# - /my/cool/celery/path:/var/run/celery
# beat:
# volumes:
# - /my/cool/celery/path:/var/run/celery

103
docker-compose.yml Normal file
View File

@ -0,0 +1,103 @@
services:
www:
build:
context: ./
image: ghcr.io/codl/forget
pull_policy: missing
restart: always
volumes:
- type: bind
source: ./config.py
target: /usr/src/app/config.py
read_only: true
depends_on:
- redis
- db
- worker
- beat
command: bash -c "
flask db upgrade &&
gunicorn -w 9 -t 3600 -b 0.0.0.0:42157 forget:app
"
networks:
- forget
expose:
- 42157
ports:
- "127.0.0.1:42157:42157"
worker:
build:
context: ./
image: ghcr.io/codl/forget
pull_policy: missing
restart: always
volumes:
- type: bind
source: ./config.py
target: /usr/src/app/config.py
read_only: true
- ./data/celery/run:/var/run/celery
depends_on:
- redis
- db
networks:
- forget
command: bash -c "
mkdir -p /var/run/celery &&
chown -R nobody:nogroup /var/run/celery &&
exec celery --app=tasks worker
--loglevel=INFO
--statedb=/var/run/celery/worker.state
--hostname=worker
--uid=nobody --gid=nogroup
"
beat:
build:
context: ./
image: ghcr.io/codl/forget
pull_policy: missing
restart: always
volumes:
- type: bind
source: ./config.py
target: /usr/src/app/config.py
read_only: true
- ./data/celery/run:/var/run/celery
depends_on:
- redis
- db
networks:
- forget
command: bash -c "
mkdir -p /var/run/celery &&
chown -R nobody:nogroup /var/run/celery &&
exec celery --app=tasks beat
--loglevel=INFO
--schedule=/var/run/celery/schedule
--uid=nobody --gid=nogroup
"
redis:
image: redis:4.0-alpine
restart: always
volumes:
- ./data/redis:/data
networks:
- forget
db:
image: postgres:14-alpine
restart: always
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=forget
volumes:
- ./data/postgres:/var/lib/postgresql/data
networks:
- forget
networks:
forget:

179
dodo.py
View File

@ -1,40 +1,101 @@
def task_gen_logo():
"""generate versions of the logo in various sizes"""
from doit import create_after
from glob import glob
from itertools import chain
def reltouch(source_filename, dest_filename):
from os import stat, utime
stat_res = stat(source_filename)
utime(dest_filename, ns=(stat_res.st_atime_ns, stat_res.st_mtime_ns))
def resize_image(basename, width, image_format):
from PIL import Image
with Image.open('assets/{}.png'.format(basename)) as im:
if 'A' in im.getbands() and image_format != 'jpeg':
im = im.convert('RGBA')
else:
im = im.convert('RGB')
height = im.height * width // im.width
new = im.resize((width, height), resample=Image.LANCZOS)
if image_format == 'jpeg':
kwargs = dict(
optimize=True,
progressive=True,
quality=80,
)
elif image_format == 'webp':
kwargs = dict(
quality=79,
)
elif image_format == 'png':
kwargs = dict(
optimize=True,
)
new.save('static/{}-{}.{}'.format(basename, width, image_format),
**kwargs)
reltouch('assets/{}.png'.format(basename),
'static/{}-{}.{}'.format(basename, width, image_format))
def resize_logo(width):
with Image.open('assets/logotype.png') as im:
im = im.convert('L')
height = im.height * width // im.width
new = im.resize((width,height), resample=Image.LANCZOS)
new.save('static/logotype-{}.png'.format(width), optimize=True)
def task_logotype():
"""resize and convert logotype"""
widths = (200, 400, 600, 800)
image_formats = ('jpeg', 'webp')
for width in widths:
yield dict(
name=str(width),
actions=[(resize_logo, (width,))],
targets=[f'static/logotype-{width}.png'],
file_dep=['assets/logotype.png'],
clean=True,
)
for image_format in image_formats:
yield dict(
name='{}.{}'.format(width, image_format),
actions=[(resize_image,
('logotype', width, image_format))],
targets=[f'static/logotype-{width}.{image_format}'],
file_dep=['assets/logotype.png'],
clean=True,
)
def task_copy_asset():
import shutil
assets = ('icon.png', 'logotype.png', 'version.js', 'settings_form.js')
def task_service_icon():
"""resize and convert service icons"""
widths = (20, 40, 80)
formats = ('webp', 'png')
for width in widths:
for image_format in formats:
for basename in ('twitter', 'mastodon', 'misskey'):
yield dict(
name='{}-{}.{}'.format(basename, width, image_format),
actions=[(resize_image, (basename, width, image_format))],
targets=[
'static/{}-{}.{}'.format(basename, width,
image_format)],
file_dep=['assets/{}.png'.format(basename)],
clean=True,
)
def task_copy():
"copy assets verbatim"
assets = ('icon.png', 'logotype.png')
def do_the_thing(src, dst):
from shutil import copy
copy(src, dst)
reltouch(src, dst)
for asset in assets:
src = 'assets/{}'.format(asset)
dst = 'static/{}'.format(asset)
yield dict(
name=asset,
actions=[(lambda asset: shutil.copy(f'assets/{asset}', f'static/{asset}'), (asset,))],
targets=[f'static/{asset}'],
file_dep=[f'assets/{asset}'],
actions=[(do_the_thing, (src, dst))],
targets=[dst],
file_dep=[src],
clean=True,
)
def task_minify_css():
"""minify css"""
"""minify css file with csscompressor"""
from csscompressor import compress
@ -42,6 +103,7 @@ def task_minify_css():
with open('assets/styles.css') as in_:
with open('static/styles.css', 'w') as out:
out.write(compress(in_.read()))
reltouch('assets/styles.css', 'static/styles.css')
return dict(
actions=[minify],
@ -50,39 +112,80 @@ def task_minify_css():
clean=True,
)
def task_compress_static():
import brotli
import gzip
files = ('static/styles.css', 'static/icon.png', 'static/logotype.png') + tuple((f'static/logotype-{width}.png' for width in (200, 400, 600, 800)))
def task_rollup():
"""rollup javascript bundle"""
def compress_brotli(dependencies):
for filename in dependencies:
with open(filename, 'rb') as in_:
with open(filename + '.br', 'wb') as out:
out.write(brotli.compress(in_.read()))
def compress_gzip(dependencies):
for filename in dependencies:
with open(filename, 'rb') as in_:
with gzip.open(filename + '.gz', 'wb') as out:
out.write(in_.read())
filenames = ['settings.js', 'instance_buttons.js']
for filename in filenames:
src = 'assets/{}'.format(filename)
dst = 'static/{}'.format(filename)
name = filename.split('.')[0]
yield dict(
name=filename,
file_dep=list(chain(
# fuck it
glob('assets/*.js'),
glob('components/*.html'))) + ['rollup.config.js'],
targets=[dst],
clean=True,
actions=[
['node_modules/.bin/rollup', '-c',
'-i', src, '-o', dst, '-n', name, '-f', 'iife'],
],
)
@create_after('logotype')
@create_after('service_icon')
@create_after('copy')
@create_after('minify_css')
@create_after('rollup')
def task_compress():
"""
make gzip and brotli compressed versions of each
static file for the server to lazily serve
"""
files = chain(
glob('static/*.css'),
glob('static/*.js'),
glob('static/*.jpeg'),
glob('static/*.png'),
glob('static/*.webp'),
)
def compress_brotli(filename):
import brotli
with open(filename, 'rb') as in_:
with open(filename + '.br', 'wb') as out:
out.write(brotli.compress(in_.read()))
reltouch(filename, filename+'.br')
def compress_gzip(filename):
import gzip
with open(filename, 'rb') as in_:
with gzip.open(filename + '.gz', 'wb') as out:
out.write(in_.read())
reltouch(filename, filename+'.gz')
for filename in files:
yield dict(
file_dep=(filename,),
targets=(filename+'.br',),
name=filename+'.br',
actions=[compress_brotli],
actions=[(compress_brotli, (filename,))],
clean=True,
)
yield dict(
file_dep=(filename,),
targets=(filename+'.gz',),
name=filename+'.gz',
actions=[compress_gzip],
actions=[(compress_gzip, (filename,))],
clean=True,
)
if __name__ == '__main__':
import doit
doit.run(globals())

View File

@ -1,2 +1,9 @@
from app import app
import routes
import routes.misc
import routes.api
assert app
assert routes
assert routes.misc
assert routes.api

View File

@ -1,7 +0,0 @@
from . import auth
from .interval import decompose_interval
from .interval import SCALES as interval_scales
from .cachebust import cachebust
from .session import set_session_cookie, get_viewer_session, get_viewer
from . import brotli
from . import settings

View File

@ -1,20 +0,0 @@
from flask import g, redirect, jsonify, make_response
from functools import wraps
def require_auth(fun):
@wraps(fun)
def wrapper(*args, **kwargs):
if not g.viewer:
return redirect('/')
return fun(*args, **kwargs)
return wrapper
def require_auth_api(fun):
@wraps(fun)
def wrapper(*args, **kwargs):
if not g.viewer:
return make_response((jsonify(status='error', error='not logged in'), 403))
return fun(*args, **kwargs)
return wrapper

View File

@ -1,66 +0,0 @@
import brotli as brotli_
from flask import request, make_response
from threading import Thread
from hashlib import sha256
import redis
import os.path
import mimetypes
class BrotliCache(object):
def __init__(self, redis_kwargs={}, max_wait=0.3, expire=60*60*12):
self.redis = redis.StrictRedis(**redis_kwargs)
self.max_wait = max_wait
self.expire = expire
def compress(self, cache_key, lock_key, body, mode=brotli_.MODE_GENERIC):
encbody = brotli_.compress(body, mode=mode)
self.redis.set(cache_key, encbody, ex=self.expire)
self.redis.delete(lock_key)
def wrap_response(self, response):
if 'br' not in request.accept_encodings or response.is_streamed:
return response
body = response.get_data()
digest = sha256(body).hexdigest()
cache_key = 'brotlicache:{}'.format(digest)
encbody = self.redis.get(cache_key)
if not encbody:
lock_key = 'brotlicache:lock:{}'.format(digest)
if self.redis.set(lock_key, 1, nx=True, ex=10):
mode = brotli_.MODE_TEXT if response.content_type.startswith('text/') else brotli_.MODE_GENERIC
t = Thread(target=self.compress, args=(cache_key, lock_key, body, mode))
t.start()
if self.max_wait > 0:
t.join(self.max_wait)
encbody = self.redis.get(cache_key)
if encbody:
response.headers.set('content-encoding', 'br')
response.headers.set('vary', 'accept-encoding')
response.set_data(encbody)
return response
return response
def brotli(app, static = True, dynamic = True):
original_static = app.view_functions['static']
def static_maybe_gzip_brotli(filename=None):
path = os.path.join(app.static_folder, filename)
for encoding, extension in (('br', '.br'), ('gzip', '.gz')):
if encoding not in request.accept_encodings:
continue
encpath = path + extension
if os.path.isfile(encpath):
resp = make_response(original_static(filename=filename + extension))
resp.headers.set('content-encoding', encoding)
resp.headers.set('vary', 'accept-encoding')
mimetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
resp.headers.set('content-type', mimetype)
return resp
return original_static(filename=filename)
if static:
app.view_functions['static'] = static_maybe_gzip_brotli
if dynamic:
cache = BrotliCache()
app.after_request(cache.wrap_response)

View File

@ -1,18 +0,0 @@
from flask import request
def set_session_cookie(session, response, secure=True):
response.set_cookie('forget_sid', session.id,
max_age=60*60*48,
httponly=True,
secure=secure)
def get_viewer_session():
from model import Session
sid = request.cookies.get('forget_sid', None)
if sid:
return Session.query.get(sid)
def get_viewer():
session = get_viewer_session()
if session:
return session.account

View File

@ -1,161 +0,0 @@
from twitter import Twitter, OAuth, TwitterHTTPError
from werkzeug.urls import url_decode
from model import OAuthToken, Account, Post
from app import db, app
from math import inf
from datetime import datetime
import locale
def get_login_url(callback='oob', consumer_key=None, consumer_secret=None):
twitter = Twitter(
auth=OAuth('', '', consumer_key, consumer_secret),
format='', api_version=None)
resp = url_decode(twitter.oauth.request_token(oauth_callback=callback))
oauth_token = resp['oauth_token']
oauth_token_secret = resp['oauth_token_secret']
token = OAuthToken(token = oauth_token, token_secret = oauth_token_secret)
db.session.merge(token)
db.session.commit()
return "https://api.twitter.com/oauth/authenticate?oauth_token=%s" % (oauth_token,)
def account_from_api_user_object(obj):
return Account(
twitter_id = obj['id_str'],
display_name = obj['name'],
screen_name = obj['screen_name'],
avatar_url = obj['profile_image_url_https'],
reported_post_count = obj['statuses_count'])
def receive_verifier(oauth_token, oauth_verifier, consumer_key=None, consumer_secret=None):
temp_token = OAuthToken.query.get(oauth_token)
if not temp_token:
raise Exception("OAuth token has expired")
twitter = Twitter(
auth=OAuth(temp_token.token, temp_token.token_secret, consumer_key, consumer_secret),
format='', api_version=None)
resp = url_decode(twitter.oauth.access_token(oauth_verifier = oauth_verifier))
db.session.delete(temp_token)
new_token = OAuthToken(token = resp['oauth_token'], token_secret = resp['oauth_token_secret'])
new_token = db.session.merge(new_token)
new_twitter = Twitter(
auth=OAuth(new_token.token, new_token.token_secret, consumer_key, consumer_secret))
remote_acct = new_twitter.account.verify_credentials()
acct = account_from_api_user_object(remote_acct)
acct = db.session.merge(acct)
new_token.account = acct
db.session.commit()
return new_token
def get_twitter_for_acc(account):
consumer_key = app.config['TWITTER_CONSUMER_KEY']
consumer_secret = app.config['TWITTER_CONSUMER_SECRET']
tokens = OAuthToken.query.with_parent(account).order_by(db.desc(OAuthToken.created_at)).all()
for token in tokens:
t = Twitter(
auth=OAuth(token.token, token.token_secret, consumer_key, consumer_secret))
try:
t.account.verify_credentials()
return t
except TwitterHTTPError as e:
if e.e.code == 401:
# token revoked
db.session.delete(token)
db.session.commit()
else:
# temporary error, re-raise
raise e
# if no tokens are valid, we log out the user so we'll get a fresh
# token when they log in again
account.force_log_out()
return None
locale.setlocale(locale.LC_TIME, 'C')
def post_from_api_tweet_object(tweet, post=None):
if not post:
post = Post()
post.twitter_id = tweet['id_str']
try:
post.created_at = datetime.strptime(tweet['created_at'], '%a %b %d %H:%M:%S %z %Y')
except ValueError:
post.created_at = datetime.strptime(tweet['created_at'], '%Y-%m-%d %H:%M:%S %z')
#whyyy
if 'full_text' in tweet:
post.body = tweet['full_text']
else:
post.body = tweet['text']
post.author_id = 'twitter:{}'.format(tweet['user']['id_str'])
if 'favorited' in tweet:
post.favourite = tweet['favorited']
if 'entities' in tweet:
post.has_media = bool('media' in tweet['entities'] and tweet['entities']['media'])
return post
def fetch_acc(account, cursor, consumer_key=None, consumer_secret=None):
t = get_twitter_for_acc(account)
if not t:
print("no twitter access, aborting")
return
user = t.account.verify_credentials()
db.session.merge(account_from_api_user_object(user))
kwargs = { 'user_id': account.twitter_id, 'count': 200, 'trim_user': True, 'tweet_mode': 'extended' }
if cursor:
kwargs.update(cursor)
if 'max_id' not in kwargs:
most_recent_post = Post.query.order_by(db.desc(Post.created_at)).filter(Post.author_id == account.id).first()
if most_recent_post:
kwargs['since_id'] = most_recent_post.twitter_id
tweets = t.statuses.user_timeline(**kwargs)
print("processing {} tweets for {acc}".format(len(tweets), acc=account))
if len(tweets) > 0:
kwargs['max_id'] = +inf
for tweet in tweets:
db.session.merge(post_from_api_tweet_object(tweet))
kwargs['max_id'] = min(tweet['id'] - 1, kwargs['max_id'])
else:
kwargs = None
db.session.commit()
return kwargs
def refresh_posts(posts):
if not posts:
return posts
t = get_twitter_for_acc(posts[0].author)
tweets = t.statuses.lookup(_id=",".join((post.twitter_id for post in posts)),
trim_user = True, tweet_mode = 'extended')
refreshed_posts = list()
for post in posts:
tweet = next((tweet for tweet in tweets if tweet['id_str'] == post.twitter_id), None)
if not tweet:
db.session.delete(post)
else:
post = db.session.merge(post_from_api_tweet_object(tweet))
refreshed_posts.append(post)
return refreshed_posts
def delete(post):
t = get_twitter_for_acc(post.author)
t.statuses.destroy(id=post.twitter_id)
db.session.delete(post)

0
libforget/__init__.py Normal file
View File

52
libforget/auth.py Normal file
View File

@ -0,0 +1,52 @@
from flask import g, redirect, jsonify, make_response, abort, request
from functools import wraps
def require_auth(fun):
@wraps(fun)
def wrapper(*args, **kwargs):
if not g.viewer:
return redirect('/')
return fun(*args, **kwargs)
return wrapper
def require_auth_api(fun):
@wraps(fun)
def wrapper(*args, **kwargs):
if not g.viewer:
return make_response((
jsonify(status='error', error='not logged in'),
403))
return fun(*args, **kwargs)
return wrapper
def csrf(fun):
@wraps(fun)
def wrapper(*args, **kwargs):
if request.form.get('csrf-token') != g.viewer.csrf_token:
return abort(403)
return fun(*args, **kwargs)
return wrapper
def set_session_cookie(session, response, secure=True):
response.set_cookie(
'forget_sid', session.id,
max_age=60*60*48,
httponly=True,
secure=secure)
def get_viewer_session():
from model import Session
sid = request.cookies.get('forget_sid', None)
if sid:
return Session.query.get(sid)
def get_viewer():
session = get_viewer_session()
if session:
return session.account

94
libforget/brotli.py Normal file
View File

@ -0,0 +1,94 @@
import brotli as brotli_
from flask import request, make_response
from threading import Thread
from hashlib import sha256
import redis as libredis
import os.path
import mimetypes
from redis.exceptions import RedisError
class BrotliCache(object):
def __init__(self, redis_uri='redis://', timeout=0.100, expire=60*60*6):
self._redis = None
self._redis_uri = redis_uri
self.timeout = timeout
self.expire = expire
@property
def redis(self):
if not self._redis:
self._redis = libredis.StrictRedis.from_url(self._redis_uri)
self._redis.client_setname('brotlicache')
return self._redis
def compress_and_cache(self, cache_key, lock_key, body, mode=brotli_.MODE_GENERIC):
encbody = brotli_.compress(body, mode=mode)
self.redis.set(cache_key, encbody, px=int(self.expire*1000))
self.redis.delete(lock_key)
def wrap_response(self, response):
if 'br' not in request.accept_encodings or response.is_streamed:
return response
body = response.get_data()
digest = sha256(body).hexdigest()
cache_key = 'brotlicache:{}'.format(digest)
try:
encbody = self.redis.get(cache_key)
response.headers.set('brotli-cache', 'HIT')
if not encbody:
response.headers.set('brotli-cache', 'MISS')
lock_key = 'brotlicache:lock:{}'.format(digest)
if self.redis.set(lock_key, 1, nx=True, ex=10):
mode = (
brotli_.MODE_TEXT
if response.content_type.startswith('text/')
else brotli_.MODE_GENERIC)
t = Thread(
target=self.compress_and_cache,
args=(cache_key, lock_key, body, mode))
t.start()
if self.timeout > 0:
t.join(self.timeout)
encbody = self.redis.get(cache_key)
if not encbody:
response.headers.set('brotli-cache', 'TIMEOUT')
else:
response.headers.set('brotli-cache', 'LOCKED')
if encbody:
response.headers.set('content-encoding', 'br')
response.headers.set('vary', 'accept-encoding')
response.set_data(encbody)
return response
except RedisError:
response.headers.set('brotli-cache', 'ERROR')
return response
def brotli(app, static=True, dynamic=True, **kwargs):
original_static = app.view_functions['static']
def static_maybe_gzip_brotli(filename=None):
path = os.path.join(app.static_folder, filename)
for encoding, extension in (('br', '.br'), ('gzip', '.gz')):
if encoding not in request.accept_encodings:
continue
encpath = path + extension
if os.path.isfile(encpath):
resp = make_response(
original_static(filename=filename + extension))
resp.headers.set('content-encoding', encoding)
resp.headers.set('vary', 'accept-encoding')
mimetype = (mimetypes.guess_type(filename)[0]
or 'application/octet-stream')
resp.headers.set('content-type', mimetype)
return resp
return original_static(filename=filename)
if static:
app.view_functions['static'] = static_maybe_gzip_brotli
if dynamic:
cache = BrotliCache(redis_uri=app.config.get('REDIS_URI'), **kwargs)
app.after_request(cache.wrap_response)

View File

@ -1,22 +1,31 @@
from flask import url_for, abort
import os
def cachebust(app):
@app.route('/static-<int:timestamp>/<path:filename>')
# pylint: disable=unused-variable
@app.route('/static-cb/<int:timestamp>/<path:filename>')
def static_cachebust(timestamp, filename):
path = os.path.join(app.static_folder, filename)
mtime = os.stat(path).st_mtime
try:
mtime = os.stat(path).st_mtime
except Exception:
return abort(404)
if abs(mtime - timestamp) > 1:
abort(404)
else:
resp = app.view_functions['static'](filename=filename)
resp.headers.set('cache-control', 'public, immutable, max-age=%s' % (60*60*24*365,))
resp.headers.set(
'cache-control',
'public, immutable, max-age={}'.format(60*60*24*365))
if 'expires' in resp.headers:
resp.headers.remove('expires')
return resp
@app.context_processor
def replace_url_for():
return dict(url_for = cachebust_url_for)
return dict(url_for=cachebust_url_for)
def cachebust_url_for(endpoint, **kwargs):
if endpoint == 'static':

6
libforget/exceptions.py Normal file
View File

@ -0,0 +1,6 @@
class PermanentError(Exception):
pass
class TemporaryError(Exception):
pass

125
libforget/img_proxy.py Normal file
View File

@ -0,0 +1,125 @@
import requests
import threading
import redis as libredis
from flask import make_response, abort
import secrets
import hmac
import base64
import pickle # nosec
import re
class ImgProxyCache(object):
def __init__(self, redis_uri='redis://', timeout=10, expire=60*60,
prefix='img_proxy', hmac_hash='sha1'):
self._redis = None
self._redis_uri = redis_uri
self.timeout = timeout
self.expire = expire
self.prefix = prefix
self.hash = hmac_hash
self.hmac_key = None
@property
def redis(self):
if not self._redis:
self._redis = libredis.StrictRedis.from_url(self._redis_uri)
self._redis.client_setname('img_proxy')
return self._redis
def key(self, *args):
return '{prefix}:1:{args}'.format(
prefix=self.prefix, args=":".join(args))
def token(self):
if not self.hmac_key:
t = self.redis.get(self.key('hmac_key'))
if not t:
t = secrets.token_urlsafe().encode('ascii')
self.redis.set(self.key('hmac_key'), t)
self.hmac_key = t
return self.hmac_key
def identifier_for(self, url):
url_hmac = hmac.new(self.token(), url.encode('UTF-8'), self.hash)
return base64.urlsafe_b64encode(
'{}:{}'.format(url_hmac.hexdigest(), url)
.encode('UTF-8')
).strip(b'=').decode('UTF-8')
def url_for(self, identifier):
try:
padding = (4 - len(identifier)) % 4
identifier += padding * '='
identifier = base64.urlsafe_b64decode(identifier).decode('UTF-8')
received_hmac, url = identifier.split(':', 1)
url_hmac = hmac.new(self.token(), url.encode('UTF-8'), self.hash)
if not hmac.compare_digest(url_hmac.hexdigest(), received_hmac):
return None
except Exception:
return None
return url
def fetch_and_cache(self, url):
resp = requests.get(url)
if(resp.status_code != 200):
return
allowed_headers = [
'content-type',
'cache-control',
'etag',
'date',
'last-modified',
]
headers = {}
expire = self.expire
if 'cache-control' in resp.headers:
for value in resp.headers['cache-control'].split(','):
match = re.match(' *max-age *= *([0-9]+) *', value)
if match:
expire = max(self.expire, int(match.group(1)))
for key in allowed_headers:
if key in resp.headers:
headers[key] = resp.headers[key]
self.redis.set(self.key('headers', url), pickle.dumps(headers, -1),
px=expire*1000)
self.redis.set(self.key('body', url),
resp.content, px=expire*1000)
def respond(self, identifier):
url = self.url_for(identifier)
if not url:
return abort(403)
x_imgproxy_cache = 'HIT'
headers = self.redis.get(self.key('headers', url))
body = self.redis.get(self.key('body', url))
if not body or not headers:
x_imgproxy_cache = 'MISS'
if self.redis.set(
self.key('lock', url), 1, nx=True, ex=10*self.timeout):
t = threading.Thread(target=self.fetch_and_cache, args=(url,))
t.start()
t.join(self.timeout)
headers = self.redis.get(self.key('headers', url))
body = self.redis.get(self.key('body', url))
try:
headers = pickle.loads(headers) # nosec
except Exception:
self.redis.delete(self.key('headers', url))
headers = None
if not body or not headers:
return abort(404)
resp = make_response(body, 200)
resp.headers.set('imgproxy-cache', x_imgproxy_cache)
resp.headers.set('cache-control', 'max-age={}'.format(self.expire))
for key, value in headers.items():
resp.headers.set(key, value)
return resp

View File

@ -1,30 +1,6 @@
from datetime import timedelta
from statistics import mean
from datetime import timedelta, datetime, timezone
from .timescales import SCALES
SCALES = [
('minutes', timedelta(minutes=1)),
('hours', timedelta(hours=1)),
('days', timedelta(days=1)),
('weeks', timedelta(days=7)),
('months', timedelta(days=
# you, a fool: a month is 30 days
# me, wise:
mean((31,
mean((29 if year % 400 == 0
or (year % 100 != 0 and year % 4 == 0)
else 28
for year in range(400)))
,31,30,31,30,31,31,30,31,30,31))
)),
('years', timedelta(days=
# you, a fool: ok. a year is 365.25 days. happy?
# me, wise: absolutely not
mean((366 if year % 400 == 0
or (year % 100 != 0 and year % 4 == 0)
else 365
for year in range(400)))
)),
]
def decompose_interval(attrname):
scales = [scale[1] for scale in SCALES]
@ -48,7 +24,7 @@ def decompose_interval(attrname):
@scale.setter
def scale(self, value):
if(type(value) != timedelta):
if not isinstance(value, timedelta):
value = timedelta(seconds=float(value))
setattr(self, attrname, max(1, getattr(self, sig_name)) * value)
@ -58,20 +34,47 @@ def decompose_interval(attrname):
@significand.setter
def significand(self, value):
if type(value) == str and value.strip() == '':
if isinstance(value, str) and value.strip() == '':
value = 0
try:
value = int(value)
assert value >= 0
except (ValueError, AssertionError) as e:
if not value >= 0:
raise ValueError(value)
except ValueError as e:
raise ValueError("Incorrect time interval", e)
setattr(self, attrname, value * getattr(self, scl_name))
setattr(cls, scl_name, scale)
setattr(cls, sig_name, significand)
return cls
return decorator
def relative(interval):
# special cases
if interval > timedelta(seconds=-15) and interval < timedelta(0):
return "just now"
elif interval > timedelta(0) and interval < timedelta(seconds=15):
return "in a few seconds"
else:
output = None
for name, scale in reversed(SCALES):
if abs(interval) > scale:
value = abs(interval) // scale
output = '{} {}'.format(value, name)
if value == 1:
output = output[:-1]
break
if not output:
output = '{} seconds'.format(abs(interval).seconds)
if interval > timedelta(0):
return 'in {}'.format(output)
else:
return '{} ago'.format(output)
def relnow(time):
return relative(time - datetime.now(timezone.utc))

23
libforget/json.py Normal file
View File

@ -0,0 +1,23 @@
from json import dumps
def account(acc):
last_delete = None
next_delete = None
if acc.last_delete:
last_delete = acc.last_delete.isoformat()
if acc.next_delete:
next_delete = acc.next_delete.isoformat()
return dumps(dict(
post_count=acc.post_count(),
eligible_for_delete_estimate=acc.estimate_eligible_for_delete(),
display_name=acc.display_name,
screen_name=acc.screen_name,
avatar_url=acc.get_avatar(),
avatar_url_orig=acc.avatar_url,
id=acc.id,
service=acc.service,
policy_enabled=acc.policy_enabled,
next_delete=next_delete,
last_delete=last_delete,
))

205
libforget/mastodon.py Normal file
View File

@ -0,0 +1,205 @@
from mastodon import Mastodon
from mastodon.Mastodon import MastodonAPIError,\
MastodonNetworkError,\
MastodonNotFoundError,\
MastodonRatelimitError,\
MastodonUnauthorizedError
from model import MastodonApp, Account, OAuthToken, Post, MastodonInstance
from requests import head
import requests
from app import db, sentry
from libforget.exceptions import TemporaryError
from functools import lru_cache
from libforget.session import make_session
def get_or_create_app(instance_url, callback, website):
instance_url = instance_url
app = MastodonApp.query.get(instance_url)
try:
head('https://{}'.format(instance_url)).raise_for_status()
proto = 'https'
except Exception:
head('http://{}'.format(instance_url)).raise_for_status()
proto = 'http'
if not app:
client_id, client_secret = Mastodon.create_app(
'forget',
scopes=('read', 'write'),
api_base_url='{}://{}'.format(proto, instance_url),
redirect_uris=callback,
website=website,
)
app = MastodonApp()
app.instance = instance_url
app.client_id = client_id
app.client_secret = client_secret
app.protocol = proto
return app
def anonymous_api(app):
return Mastodon(
app.client_id,
client_secret=app.client_secret,
api_base_url='{}://{}'.format(app.protocol, app.instance),
session=make_session(),
)
def login_url(app, callback):
return anonymous_api(app).auth_request_url(
redirect_uris=callback,
scopes=('read', 'write',)
)
def receive_code(code, app, callback):
api = anonymous_api(app)
access_token = api.log_in(
code=code,
scopes=('read', 'write'),
redirect_uri=callback,
)
remote_acc = api.account_verify_credentials()
acc = account_from_api_object(remote_acc, app.instance)
acc = db.session.merge(acc)
token = OAuthToken(token=access_token)
token = db.session.merge(token)
token.account = acc
return token
@lru_cache()
def get_api_for_acc(account):
app = MastodonApp.query.get(account.mastodon_instance)
for token in account.tokens:
api = Mastodon(
app.client_id,
client_secret=app.client_secret,
api_base_url='{}://{}'.format(app.protocol, app.instance),
access_token=token.token,
ratelimit_method='throw',
session=make_session(),
)
try:
# api.verify_credentials()
# doesnt error even if the token is revoked lol
# https://github.com/tootsuite/mastodon/issues/4637
# so we have to do this:
api.timeline()
if api.ratelimit_remaining / api.ratelimit_limit < 1/4:
raise TemporaryError("Rate limit too low")
return api
except MastodonUnauthorizedError as e:
if sentry:
sentry.captureMessage(
'Mastodon auth revoked or incorrect',
extra=locals())
db.session.delete(token)
db.session.commit()
continue
except MastodonAPIError as e:
raise TemporaryError(e)
except (MastodonNetworkError,
MastodonRatelimitError) as e:
raise TemporaryError(e)
raise TemporaryError('No access to account {}'.format(account))
def fetch_posts(acc, max_id, since_id):
api = get_api_for_acc(acc)
try:
newacc = account_from_api_object(
api.account_verify_credentials(), acc.mastodon_instance)
acc = db.session.merge(newacc)
kwargs = dict(limit=40)
if max_id:
kwargs['max_id'] = max_id
if since_id:
kwargs['since_id'] = since_id
statuses = api.account_statuses(acc.mastodon_id, **kwargs)
return [post_from_api_object(status, acc.mastodon_instance) for status in statuses]
except (MastodonAPIError,
MastodonNetworkError,
MastodonRatelimitError) as e:
raise TemporaryError(e)
def post_from_api_object(obj, instance):
return Post(
mastodon_instance=instance,
mastodon_id=obj['id'],
favourite=obj['favourited'],
has_media=('media_attachments' in obj
and bool(obj['media_attachments'])),
created_at=obj['created_at'],
author_id=account_from_api_object(obj['account'], instance).id,
direct=obj['visibility'] == 'direct',
is_reblog=obj['reblog'] is not None,
)
def account_from_api_object(obj, instance):
return Account(
mastodon_instance=instance,
mastodon_id=obj['id'],
screen_name='{}@{}'.format(obj['username'], instance),
display_name=obj['display_name'],
avatar_url=obj['avatar'],
reported_post_count=obj['statuses_count'],
)
def refresh_posts(posts):
acc = posts[0].author
api = get_api_for_acc(acc)
new_posts = list()
with db.session.no_autoflush:
for post in posts:
print('Refreshing {}'.format(post))
try:
status = api.status(post.mastodon_id)
new_post = db.session.merge(
post_from_api_object(status, post.mastodon_instance))
new_post.touch()
new_posts.append(new_post)
except MastodonNotFoundError:
db.session.delete(post)
except (MastodonAPIError,
MastodonNetworkError,
MastodonRatelimitError) as e:
raise TemporaryError(e)
return new_posts
def delete(post):
api = get_api_for_acc(post.author)
try:
api.status_delete(post.mastodon_id)
db.session.delete(post)
except (MastodonAPIError,
MastodonNetworkError,
MastodonRatelimitError) as e:
raise TemporaryError(e)
def suggested_instances(limit=5, min_popularity=5, blocklist=tuple()):
return tuple((ins.instance for ins in (
MastodonInstance.query
.filter(MastodonInstance.popularity > min_popularity)
.filter(~MastodonInstance.instance.in_(blocklist))
.order_by(db.desc(MastodonInstance.popularity),
MastodonInstance.instance)
.limit(limit).all())))

191
libforget/misskey.py Normal file
View File

@ -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': ['write:notes', 'read:reactions'],
'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())))

14
libforget/session.py Normal file
View File

@ -0,0 +1,14 @@
import requests
import version
def make_session():
s = requests.Session()
s.headers.update(
{
"user-agent": "Forget/{version} +https://forget.codl.fr".format(
version=version.get_versions()["version"]
)
}
)
return s

View File

@ -6,4 +6,6 @@ attrs = (
'policy_keep_younger_scale',
'policy_keep_younger_significand',
'policy_keep_media',
'policy_keep_direct',
'policy_enabled',
)

29
libforget/timescales.py Normal file
View File

@ -0,0 +1,29 @@
# flake8: noqa
from datetime import timedelta
from statistics import mean
SCALES = [
('minutes', timedelta(minutes=1)),
('hours', timedelta(hours=1)),
('days', timedelta(days=1)),
('weeks', timedelta(days=7)),
('months', timedelta(days=
# you, a fool: a month is 30 days
# me, wise:
mean((31,
mean((29 if year % 400 == 0
or (year % 100 != 0 and year % 4 == 0)
else 28
for year in range(400)))
,31,30,31,30,31,31,30,31,30,31))
)),
('years', timedelta(days=
# you, a fool: ok. a year is 365.25 days. happy?
# me, wise: absolutely not
mean((366 if year % 400 == 0
or (year % 100 != 0 and year % 4 == 0)
else 365
for year in range(400)))
)),
]

201
libforget/twitter.py Normal file
View File

@ -0,0 +1,201 @@
from twitter import Twitter, OAuth, TwitterHTTPError, TwitterError
from werkzeug.urls import url_decode
from model import OAuthToken, Account, Post, TwitterArchive
from app import db, app, sentry
from math import inf
from datetime import datetime
import locale
from zipfile import ZipFile
from io import BytesIO
from libforget.exceptions import PermanentError, TemporaryError
from urllib.error import URLError
def get_login_url(callback='oob', consumer_key=None, consumer_secret=None):
twitter = Twitter(
auth=OAuth('', '', consumer_key, consumer_secret),
format='', api_version=None)
resp = url_decode(twitter.oauth.request_token(oauth_callback=callback))
oauth_token = resp['oauth_token']
oauth_token_secret = resp['oauth_token_secret']
token = OAuthToken(token=oauth_token, token_secret=oauth_token_secret)
db.session.merge(token)
db.session.commit()
return (
"https://api.twitter.com/oauth/authenticate?oauth_token=%s"
% (oauth_token,))
def account_from_api_user_object(obj):
return Account(
twitter_id=obj['id_str'],
display_name=obj['name'],
screen_name=obj['screen_name'],
avatar_url=obj['profile_image_url_https'],
reported_post_count=obj['statuses_count'])
def receive_verifier(oauth_token, oauth_verifier,
consumer_key=None, consumer_secret=None):
temp_token = OAuthToken.query.get(oauth_token)
if not temp_token:
raise Exception("OAuth token has expired")
twitter = Twitter(
auth=OAuth(temp_token.token, temp_token.token_secret,
consumer_key, consumer_secret),
format='', api_version=None)
resp = url_decode(
twitter.oauth.access_token(oauth_verifier=oauth_verifier))
db.session.delete(temp_token)
new_token = OAuthToken(token=resp['oauth_token'],
token_secret=resp['oauth_token_secret'])
new_token = db.session.merge(new_token)
new_twitter = Twitter(
auth=OAuth(new_token.token, new_token.token_secret,
consumer_key, consumer_secret))
remote_acct = new_twitter.account.verify_credentials()
acct = account_from_api_user_object(remote_acct)
acct = db.session.merge(acct)
new_token.account = acct
db.session.commit()
return new_token
def get_twitter_for_acc(account):
consumer_key = app.config['TWITTER_CONSUMER_KEY']
consumer_secret = app.config['TWITTER_CONSUMER_SECRET']
tokens = (OAuthToken.query.with_parent(account)
.order_by(db.desc(OAuthToken.created_at)).all())
for token in tokens:
t = Twitter(
auth=OAuth(token.token, token.token_secret,
consumer_key, consumer_secret))
try:
t.account.verify_credentials()
return t
except TwitterHTTPError as e:
if e.e.code == 401:
# token revoked
if sentry:
sentry.captureMessage(
'Twitter auth revoked', extra=locals())
db.session.delete(token)
db.session.commit()
else:
raise TemporaryError(e)
except URLError as e:
raise TemporaryError(e)
raise TemporaryError("No access to account {}".format(account))
locale.setlocale(locale.LC_TIME, 'C')
def post_from_api_tweet_object(tweet, post=None):
if not post:
post = Post()
post.twitter_id = tweet['id_str']
try:
post.created_at = datetime.strptime(
tweet['created_at'], '%a %b %d %H:%M:%S %z %Y')
except ValueError:
post.created_at = datetime.strptime(
tweet['created_at'], '%Y-%m-%d %H:%M:%S %z')
# whyyy
post.author_id = 'twitter:{}'.format(tweet['user']['id_str'])
if 'favorited' in tweet:
post.favourite = tweet['favorited']
if 'entities' in tweet:
post.has_media = bool(
'media' in tweet['entities'] and tweet['entities']['media'])
post.is_reblog = 'retweeted_status' in tweet
return post
def fetch_posts(account, max_id, since_id):
t = get_twitter_for_acc(account)
try:
user = t.account.verify_credentials()
db.session.merge(account_from_api_user_object(user))
kwargs = {
'user_id': account.twitter_id,
'count': 200,
'trim_user': True,
'tweet_mode': 'extended',
}
if max_id:
kwargs['max_id'] = max_id
if since_id:
kwargs['since_id'] = since_id
tweets = t.statuses.user_timeline(**kwargs)
except (TwitterError, URLError) as e:
handle_error(e)
return [post_from_api_tweet_object(tweet) for tweet in tweets]
def refresh_posts(posts):
if not posts:
return posts
t = get_twitter_for_acc(posts[0].author)
try:
tweets = t.statuses.lookup(
_id=",".join((post.twitter_id for post in posts)),
trim_user=True, tweet_mode='extended')
except (URLError, TwitterError) as e:
handle_error(e)
refreshed_posts = list()
for post in posts:
tweet = next(
(tweet for tweet in tweets if tweet['id_str'] == post.twitter_id),
None)
if not tweet:
db.session.delete(post)
else:
post = db.session.merge(post_from_api_tweet_object(tweet))
post.touch()
refreshed_posts.append(post)
return refreshed_posts
def delete(post):
t = get_twitter_for_acc(post.author)
t.statuses.destroy(id=post.twitter_id)
db.session.delete(post)
def chunk_twitter_archive(archive_id):
ta = TwitterArchive.query.get(archive_id)
with ZipFile(BytesIO(ta.body), 'r') as zipfile:
files = [filename for filename in zipfile.namelist()
if filename.startswith('data/js/tweets/')
and filename.endswith('.js')]
files.sort()
return files
def handle_error(e):
if isinstance(e, TwitterHTTPError):
data = e.response_data
if isinstance(data, dict) and 'errors' in data.keys():
for error in data['errors']:
if error.get('code',0) == 326:
# account locked lol rip
# although this is a temporary error in twitter terms
# it's best not to waste api calls on locked accounts
raise PermanentError(e)
raise TemporaryError(e)

4
libforget/version.py Normal file
View File

@ -0,0 +1,4 @@
from app import app
def url_for_version(ver):
return app.config['CHANGELOG_URL'].format(hash=ver['full-revisionid'])

View File

@ -0,0 +1,53 @@
"""add three-way favourite policy
Revision ID: 2bd33abe291c
Revises: 583cdac8eba1
Create Date: 2018-01-03 17:31:03.718648
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '2bd33abe291c'
down_revision = '583cdac8eba1'
branch_labels = None
depends_on = None
transitional = sa.table('accounts',
sa.column('policy_keep_favourites'),
sa.column('old_policy_keep_favourites'))
def upgrade():
ThreeWayPolicyEnum = sa.Enum('keeponly', 'deleteonly', 'none',
name='enum_3way_policy')
op.alter_column('accounts', 'policy_keep_favourites',
new_column_name='old_policy_keep_favourites')
op.add_column(
'accounts',
sa.Column('policy_keep_favourites', ThreeWayPolicyEnum,
nullable=False, server_default='none'))
op.execute(transitional.update()
.where(transitional.c.old_policy_keep_favourites)
.values(policy_keep_favourites=op.inline_literal('keeponly')))
op.drop_column('accounts', 'old_policy_keep_favourites')
def downgrade():
op.alter_column('accounts', 'policy_keep_favourites',
new_column_name='old_policy_keep_favourites')
op.add_column(
'accounts',
sa.Column('policy_keep_favourites', sa.Boolean(),
nullable=False, server_default='f'))
op.execute(transitional.update()
.where(transitional.c.old_policy_keep_favourites == op.inline_literal('keeponly'))
.values(policy_keep_favourites=op.inline_literal('t')))
op.drop_column('accounts', 'old_policy_keep_favourites')

View File

@ -0,0 +1,24 @@
"""add reason to account
Revision ID: 3a0138499994
Revises: 41ef02e66382
Create Date: 2017-09-02 19:46:14.035946
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '3a0138499994'
down_revision = '41ef02e66382'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('accounts', sa.Column('reason', sa.String(), nullable=True))
def downgrade():
op.drop_column('accounts', 'reason')

View File

@ -0,0 +1,26 @@
"""default next_delete to null
Revision ID: 41ef02e66382
Revises: f95af1a8d89f
Create Date: 2017-08-31 21:19:44.304952
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '41ef02e66382'
down_revision = 'f95af1a8d89f'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column('accounts', 'next_delete', server_default=None)
op.execute("UPDATE accounts SET next_delete = NULL where next_delete = 'epoch';")
def downgrade():
op.alter_column('accounts', 'next_delete', server_default='epoch')
op.execute("UPDATE accounts SET next_delete = 'epoch' where next_delete IS NULL;")

View File

@ -0,0 +1,28 @@
"""new fetching flags
Revision ID: 4b56cde3ebd7
Revises: c136aa1157f9
Create Date: 2019-02-24 11:53:29.128983
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4b56cde3ebd7'
down_revision = 'c136aa1157f9'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('accounts', sa.Column('fetch_current_batch_end_id', sa.String(), nullable=True))
op.add_column('accounts', sa.Column('fetch_history_complete', sa.Boolean(), server_default='FALSE', nullable=False))
op.create_foreign_key(op.f('fk_accounts_fetch_current_batch_end_id_posts'), 'accounts', 'posts', ['fetch_current_batch_end_id'], ['id'], ondelete='SET NULL')
def downgrade():
op.drop_constraint(op.f('fk_accounts_fetch_current_batch_end_id_posts'), 'accounts', type_='foreignkey')
op.drop_column('accounts', 'fetch_history_complete')
op.drop_column('accounts', 'fetch_current_batch_end_id')

View File

@ -0,0 +1,59 @@
"""add three-way media policy
Revision ID: 583cdac8eba1
Revises: 7e255d4ea34d
Create Date: 2017-12-28 00:46:56.023649
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '583cdac8eba1'
down_revision = '7e255d4ea34d'
branch_labels = None
depends_on = None
transitional = sa.table('accounts',
sa.column('policy_keep_media'),
sa.column('old_policy_keep_media'))
def upgrade():
ThreeWayPolicyEnum = sa.Enum('keeponly', 'deleteonly', 'none',
name='enum_3way_policy')
op.execute("""
CREATE TYPE enum_3way_policy AS ENUM ('keeponly', 'deleteonly', 'none')
""")
op.alter_column('accounts', 'policy_keep_media',
new_column_name='old_policy_keep_media')
op.add_column(
'accounts',
sa.Column('policy_keep_media', ThreeWayPolicyEnum,
nullable=False, server_default='none'))
op.execute(transitional.update()
.where(transitional.c.old_policy_keep_media)
.values(policy_keep_media=op.inline_literal('keeponly')))
op.drop_column('accounts', 'old_policy_keep_media')
def downgrade():
op.alter_column('accounts', 'policy_keep_media',
new_column_name='old_policy_keep_media')
op.add_column(
'accounts',
sa.Column('policy_keep_media', sa.Boolean(),
nullable=False, server_default='f'))
op.execute(transitional.update()
.where(transitional.c.old_policy_keep_media == op.inline_literal('keeponly'))
.values(policy_keep_media=op.inline_literal('t')))
op.drop_column('accounts', 'old_policy_keep_media')
op.execute("""
DROP TYPE enum_3way_policy
""")

View File

@ -0,0 +1,26 @@
"""add post.direct and account.policy_keep_direct
Revision ID: 5fec5f5e8a5e
Revises: 8993e80e7aa3
Create Date: 2017-08-20 18:16:26.682744
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '5fec5f5e8a5e'
down_revision = '8993e80e7aa3'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('accounts', sa.Column('policy_keep_direct', sa.Boolean(), server_default='TRUE', nullable=False))
op.add_column('posts', sa.Column('direct', sa.Boolean(), server_default='FALSE', nullable=False))
def downgrade():
op.drop_column('posts', 'direct')
op.drop_column('accounts', 'policy_keep_direct')

View File

@ -0,0 +1,34 @@
"""add some probably good indexes (???)
Revision ID: 6d298e6406f2
Revises: 8fac6e10bdb3
Create Date: 2017-08-14 20:27:49.103672
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6d298e6406f2'
down_revision = '8fac6e10bdb3'
branch_labels = None
depends_on = None
def upgrade():
op.create_index(op.f('ix_accounts_last_delete'), 'accounts', ['last_delete'], unique=False)
op.create_index(op.f('ix_accounts_last_fetch'), 'accounts', ['last_fetch'], unique=False)
op.create_index(op.f('ix_accounts_last_refresh'), 'accounts', ['last_refresh'], unique=False)
op.create_index(op.f('ix_oauth_tokens_account_id'), 'oauth_tokens', ['account_id'], unique=False)
op.create_index(op.f('ix_posts_author_id'), 'posts', ['author_id'], unique=False)
op.create_index(op.f('ix_sessions_account_id'), 'sessions', ['account_id'], unique=False)
def downgrade():
op.drop_index(op.f('ix_sessions_account_id'), table_name='sessions')
op.drop_index(op.f('ix_posts_author_id'), table_name='posts')
op.drop_index(op.f('ix_oauth_tokens_account_id'), table_name='oauth_tokens')
op.drop_index(op.f('ix_accounts_last_refresh'), table_name='accounts')
op.drop_index(op.f('ix_accounts_last_fetch'), table_name='accounts')
op.drop_index(op.f('ix_accounts_last_delete'), table_name='accounts')

View File

@ -0,0 +1,26 @@
"""add last_delete back
Revision ID: 6fd1f5b43824
Revises: d97fa46b5560
Create Date: 2017-08-29 17:22:00.747220
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6fd1f5b43824'
down_revision = 'd97fa46b5560'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('accounts', sa.Column('last_delete', sa.DateTime(), nullable=True))
op.create_index(op.f('ix_accounts_last_delete'), 'accounts', ['last_delete'], unique=False)
def downgrade():
op.drop_index(op.f('ix_accounts_last_delete'), table_name='accounts')
op.drop_column('accounts', 'last_delete')

View File

@ -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;')

View File

@ -0,0 +1,32 @@
"""add mastodon apps
Revision ID: 7afc7b343323
Revises: f63bf9e73bc9
Create Date: 2017-08-18 20:36:00.104508
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7afc7b343323'
down_revision = 'f63bf9e73bc9'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('mastodon_app',
sa.Column('created_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('now()'), nullable=False),
sa.Column('instance', sa.String(), nullable=False),
sa.Column('client_id', sa.String(), nullable=False),
sa.Column('client_secret', sa.String(), nullable=False),
sa.Column('protocol', sa.Enum('http', 'https', name='enum_protocol'), nullable=False),
sa.PrimaryKeyConstraint('instance', name=op.f('pk_mastodon_app'))
)
def downgrade():
op.drop_table('mastodon_app')

View File

@ -0,0 +1,64 @@
"""remove fetch batch fk
things are real bad if the associated post is deleted and this is nulled
keeping an opaque ID and associated date should work fine
see GH-584
Revision ID: 7b0e9b8e0887
Revises: 740fe24a7712
Create Date: 2022-02-27 11:48:55.107299
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7b0e9b8e0887'
down_revision = '740fe24a7712'
branch_labels = None
depends_on = None
def upgrade():
op.drop_constraint('fk_accounts_fetch_current_batch_end_id_posts', 'accounts', type_='foreignkey')
op.add_column('accounts', sa.Column('fetch_current_batch_end_date', sa.DateTime(timezone=True), nullable=True))
op.execute('''
UPDATE accounts SET fetch_current_batch_end_date = posts.created_at
FROM posts WHERE accounts.fetch_current_batch_end_id = posts.id;
''')
# update ids from "mastodon:69420@chitter.xyz" format to just "69420"
op.execute('''
UPDATE accounts SET fetch_current_batch_end_id =
split_part(
split_part(fetch_current_batch_end_id, ':', 2),
'@', 1);
''')
def downgrade():
# converts ids like "69420" back to "mastodon:69420@chitter.xyz"
# i sure hope there isn't a mastodon-compatible out there that can have
# : or @ in its post ids
op.execute('''
WITH accounts_exploded_ids AS (
SELECT
id,
split_part(id, ':', 1) || ':' AS service,
CASE WHEN position('@' IN id) != 0
THEN '@' || split_part(id, @, 2)
ELSE ''
END as instance
FROM accounts
)
UPDATE accounts SET fetch_current_batch_end_id = e.service || fetch_current_batch_end_id || e.instance
FROM accounts_exploded_ids AS e WHERE e.id = accounts.id AND fetch_current_batch_end_id IS NOT NULL;
''')
op.execute('''
UPDATE accounts SET fetch_current_batch_end_id = NULL
WHERE NOT EXISTS (SELECT 1 FROM posts WHERE fetch_current_batch_end_id = posts.id);
''')
op.create_foreign_key('fk_accounts_fetch_current_batch_end_id_posts', 'accounts', 'posts', ['fetch_current_batch_end_id'], ['id'], ondelete='SET NULL')
op.drop_column('accounts', 'fetch_current_batch_end_date')

View File

@ -0,0 +1,24 @@
"""add is_reblog to Post
Revision ID: 7e255d4ea34d
Revises: 83510ef8c1a5
Create Date: 2017-12-27 21:18:48.988601
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7e255d4ea34d'
down_revision = '83510ef8c1a5'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('posts', sa.Column('is_reblog', sa.Boolean(), server_default='FALSE', nullable=False))
def downgrade():
op.drop_column('posts', 'is_reblog')

View File

@ -0,0 +1,26 @@
"""add favourite, reblog count to posts
Revision ID: 83510ef8c1a5
Revises: c1f7444d0f75
Create Date: 2017-12-27 20:40:31.576201
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '83510ef8c1a5'
down_revision = 'c1f7444d0f75'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('posts', sa.Column('favourites', sa.Integer(), nullable=True))
op.add_column('posts', sa.Column('reblogs', sa.Integer(), nullable=True))
def downgrade():
op.drop_column('posts', 'reblogs')
op.drop_column('posts', 'favourites')

View File

@ -0,0 +1,24 @@
"""remove post body
Revision ID: 8993e80e7aa3
Revises: c80af843eed3
Create Date: 2017-08-20 18:04:28.516129
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8993e80e7aa3'
down_revision = 'c80af843eed3'
branch_labels = None
depends_on = None
def upgrade():
op.drop_column('posts', 'body')
def downgrade():
op.add_column('posts', sa.Column('body', sa.VARCHAR(), autoincrement=False, nullable=True))

View File

@ -0,0 +1,24 @@
"""add last_refresh to match last_fetch and last_delete
Revision ID: 8fac6e10bdb3
Revises: 04da9abf37e2
Create Date: 2017-08-12 22:55:33.004791
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8fac6e10bdb3'
down_revision = '04da9abf37e2'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('accounts', sa.Column('last_refresh', sa.DateTime(), server_default='epoch', nullable=True))
def downgrade():
op.drop_column('accounts', 'last_refresh')

View File

@ -0,0 +1,30 @@
"""change timestamps to timestamptzs
Revision ID: 90b5b84abc6a
Revises: 6fd1f5b43824
Create Date: 2017-08-31 16:46:16.785021
"""
from alembic import op
from sqlalchemy.types import DateTime
# revision identifiers, used by Alembic.
revision = '90b5b84abc6a'
down_revision = '6fd1f5b43824'
branch_labels = None
depends_on = None
def upgrade():
for table in ('accounts', 'oauth_tokens', 'posts', 'sessions',
'twitter_archives', 'mastodon_apps'):
for column in ('created_at', 'updated_at'):
op.alter_column(table, column, type_=DateTime(timezone=True))
def downgrade():
for table in ('account', 'oauth_tokens', 'posts', 'sessions',
'twitter_archives', 'mastodon_apps'):
for column in ('created_at', 'updated_at'):
op.alter_column(table, column, type_=DateTime(timezone=False))

View File

@ -0,0 +1,26 @@
"""remove favourite, reblog count from posts
Revision ID: af763dccc0b4
Revises: 4b56cde3ebd7
Create Date: 2021-05-14 19:45:37.429645
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'af763dccc0b4'
down_revision = '4b56cde3ebd7'
branch_labels = None
depends_on = None
def upgrade():
op.drop_column('posts', 'reblogs')
op.drop_column('posts', 'favourites')
def downgrade():
op.add_column('posts', sa.Column('favourites', sa.Integer(), nullable=True))
op.add_column('posts', sa.Column('reblogs', sa.Integer(), nullable=True))

View File

@ -0,0 +1,26 @@
"""add backoff
Revision ID: c136aa1157f9
Revises: 2bd33abe291c
Create Date: 2018-07-06 00:13:29.726250
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c136aa1157f9'
down_revision = '2bd33abe291c'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('accounts', sa.Column('backoff_level', sa.Integer(), server_default='0', nullable=False))
op.add_column('accounts', sa.Column('backoff_until', sa.DateTime(timezone=True), server_default='now', nullable=False))
def downgrade():
op.drop_column('accounts', 'backoff_until')
op.drop_column('accounts', 'backoff_level')

View File

@ -0,0 +1,24 @@
"""add dormant to account
Revision ID: c1f7444d0f75
Revises: 3a0138499994
Create Date: 2017-09-04 21:57:23.648580
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c1f7444d0f75'
down_revision = '3a0138499994'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('accounts', sa.Column('dormant', sa.Boolean(), server_default='FALSE', nullable=False))
def downgrade():
op.drop_column('accounts', 'dormant')

View File

@ -0,0 +1,28 @@
"""make token secret nullable
Revision ID: c80af843eed3
Revises: fbdc10b29df9
Create Date: 2017-08-18 21:25:17.933702
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'c80af843eed3'
down_revision = 'fbdc10b29df9'
branch_labels = None
depends_on = None
def upgrade():
op.alter_column('oauth_tokens', 'token_secret',
existing_type=sa.VARCHAR(),
nullable=True)
def downgrade():
op.alter_column('oauth_tokens', 'token_secret',
existing_type=sa.VARCHAR(),
nullable=False)

View File

@ -0,0 +1,26 @@
"""add csrf tokens
Revision ID: d97fa46b5560
Revises: f8a153bc809b
Create Date: 2017-08-25 10:10:18.148120
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'd97fa46b5560'
down_revision = 'f8a153bc809b'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('sessions', sa.Column('csrf_token', sa.String(), nullable=True))
op.execute('DELETE FROM sessions')
op.alter_column('sessions', 'csrf_token', nullable=False)
def downgrade():
op.drop_column('sessions', 'csrf_token')

View File

@ -0,0 +1,32 @@
"""replace last_delete with next_delete
Revision ID: e769c033e5c9
Revises: 6d298e6406f2
Create Date: 2017-08-14 20:51:02.248343
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'e769c033e5c9'
down_revision = '6d298e6406f2'
branch_labels = None
depends_on = None
def upgrade():
op.add_column('accounts', sa.Column('next_delete', sa.DateTime(), server_default='epoch', nullable=True))
op.execute('UPDATE accounts SET next_delete = last_delete + policy_delete_every;')
op.create_index(op.f('ix_accounts_next_delete'), 'accounts', ['next_delete'], unique=False)
op.drop_index('ix_accounts_last_delete', table_name='accounts')
op.drop_column('accounts', 'last_delete')
def downgrade():
op.add_column('accounts', sa.Column('last_delete', postgresql.TIMESTAMP(), server_default=sa.text("'1970-01-01 00:00:00'::timestamp without time zone"), autoincrement=False, nullable=True))
op.execute('UPDATE accounts SET last__delete = next_delete - policy_delete_every;')
op.create_index('ix_accounts_last_delete', 'accounts', ['last_delete'], unique=False)
op.drop_index(op.f('ix_accounts_next_delete'), table_name='accounts')
op.drop_column('accounts', 'next_delete')

View File

@ -0,0 +1,26 @@
"""replace index on posts.author_id with composite index on author_id and created_at
Revision ID: f63bf9e73bc9
Revises: e769c033e5c9
Create Date: 2017-08-15 23:55:46.945437
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f63bf9e73bc9'
down_revision = 'e769c033e5c9'
branch_labels = None
depends_on = None
def upgrade():
op.create_index('ix_posts_author_id_created_at', 'posts', ['author_id', 'created_at'], unique=False)
op.drop_index('ix_posts_author_id', table_name='posts')
def downgrade():
op.create_index('ix_posts_author_id', 'posts', ['author_id'], unique=False)
op.drop_index('ix_posts_author_id_created_at', table_name='posts')

View File

@ -0,0 +1,40 @@
"""add mastodon_instances
Revision ID: f8a153bc809b
Revises: 5fec5f5e8a5e
Create Date: 2017-08-23 11:27:19.223721
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'f8a153bc809b'
down_revision = '5fec5f5e8a5e'
branch_labels = None
depends_on = None
def upgrade():
op.create_table('mastodon_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_mastodon_instances'))
)
op.execute("""
INSERT INTO mastodon_instances (instance, popularity) VALUES
('mastodon.social', 100),
('mastodon.cloud', 90),
('social.tchncs.de', 80),
('mastodon.xyz', 70),
('mstdn.io', 60),
('awoo.space', 50),
('cybre.space', 40),
('mastodon.art', 30)
;
""")
def downgrade():
op.drop_table('mastodon_instances')

View File

@ -0,0 +1,26 @@
"""change more timestamps to timestamptzs
Revision ID: f95af1a8d89f
Revises: 90b5b84abc6a
Create Date: 2017-08-31 17:00:20.538070
"""
from alembic import op
from sqlalchemy.types import DateTime
# revision identifiers, used by Alembic.
revision = 'f95af1a8d89f'
down_revision = '90b5b84abc6a'
branch_labels = None
depends_on = None
def upgrade():
for column in ('last_fetch', 'last_refresh', 'last_delete', 'next_delete'):
op.alter_column('accounts', column, type_=DateTime(timezone=True))
def downgrade():
for column in ('last_fetch', 'last_refresh', 'last_delete', 'next_delete'):
op.alter_column('accounts', column, type_=DateTime(timezone=False))

View File

@ -0,0 +1,24 @@
"""it's supposed to be plural, dummy
Revision ID: fbdc10b29df9
Revises: 7afc7b343323
Create Date: 2017-08-18 20:39:39.119165
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = 'fbdc10b29df9'
down_revision = '7afc7b343323'
branch_labels = None
depends_on = None
def upgrade():
op.execute('ALTER TABLE mastodon_app RENAME TO mastodon_apps')
def downgrade():
op.execute('ALTER TABLE mastodon_apps RENAME TO mastodon_app')

350
model.py
View File

@ -1,18 +1,23 @@
from datetime import datetime
from datetime import timedelta, datetime, timezone
from app import db
from twitter import Twitter, OAuth
import secrets
from lib import decompose_interval
from datetime import timedelta
from libforget.interval import decompose_interval
import random
from sqlalchemy.ext.declarative import declared_attr
class TimestampMixin(object):
created_at = db.Column(db.DateTime, server_default=db.func.now(), nullable=False)
updated_at = db.Column(db.DateTime, server_default=db.func.now(), onupdate=db.func.now(), nullable=False)
created_at = db.Column(
db.DateTime(timezone=True), server_default=db.func.now(),
nullable=False)
updated_at = db.Column(
db.DateTime(timezone=True), server_default=db.func.now(),
onupdate=db.func.now(), nullable=False)
def touch(self):
self.updated_at=db.func.now()
self.updated_at = db.func.now()
class RemoteIDMixin(object):
@property
@ -26,12 +31,83 @@ class RemoteIDMixin(object):
if not self.id:
return None
if self.service != "twitter":
raise Exception("tried to get twitter id for a {} {}".format(self.service, type(self)))
raise Exception(
"tried to get twitter id for a {} {}"
.format(self.service, type(self)))
return self.id.split(":")[1]
@twitter_id.setter
def twitter_id(self, id):
self.id = "twitter:{}".format(id)
def twitter_id(self, id_):
self.id = "twitter:{}".format(id_)
@property
def mastodon_instance(self):
if not self.id:
return None
if self.service != "mastodon":
raise Exception(
"tried to get mastodon instance for a {} {}"
.format(self.service, type(self)))
return self.id.split(":", 1)[1].split('@')[1]
@mastodon_instance.setter
def mastodon_instance(self, instance):
self.id = "mastodon:{}@{}".format(self.mastodon_id, instance)
@property
def mastodon_id(self):
if not self.id:
return None
if self.service != "mastodon":
raise Exception(
"tried to get mastodon id for a {} {}"
.format(self.service, type(self)))
return self.id.split(":", 1)[1].split('@')[0]
@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):
if self.service == 'twitter':
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',
name='enum_3way_policy')
@decompose_interval('policy_delete_every')
@ -40,128 +116,241 @@ class Account(TimestampMixin, RemoteIDMixin):
__tablename__ = 'accounts'
id = db.Column(db.String, primary_key=True)
policy_enabled = db.Column(db.Boolean, server_default='FALSE', nullable=False)
policy_keep_latest = db.Column(db.Integer, server_default='100', nullable=False)
policy_keep_favourites = db.Column(db.Boolean, server_default='TRUE', nullable=False)
policy_keep_media = db.Column(db.Boolean, server_default='FALSE', nullable=False)
policy_delete_every = db.Column(db.Interval, server_default='30 minutes', nullable=False)
policy_keep_younger = db.Column(db.Interval, server_default='365 days', nullable=False)
policy_enabled = db.Column(db.Boolean, server_default='FALSE',
nullable=False)
policy_keep_latest = db.Column(db.Integer, server_default='100',
nullable=False)
policy_keep_favourites = db.Column(ThreeWayPolicyEnum,
server_default='none', nullable=False)
policy_keep_media = db.Column(ThreeWayPolicyEnum, server_default='none',
nullable=False)
policy_delete_every = db.Column(db.Interval, server_default='30 minutes',
nullable=False)
policy_keep_younger = db.Column(db.Interval, server_default='365 days',
nullable=False)
policy_keep_direct = db.Column(db.Boolean, server_default='TRUE',
nullable=False)
display_name = db.Column(db.String)
screen_name = db.Column(db.String)
avatar_url = db.Column(db.String)
reported_post_count = db.Column(db.Integer)
last_fetch = db.Column(db.DateTime, server_default='epoch')
last_delete = db.Column(db.DateTime, server_default='epoch')
last_fetch = db.Column(db.DateTime(timezone=True),
server_default='epoch', index=True)
last_refresh = db.Column(db.DateTime(timezone=True),
server_default='epoch', index=True)
last_delete = db.Column(db.DateTime(timezone=True), index=True)
next_delete = db.Column(db.DateTime(timezone=True), index=True)
fetch_history_complete = db.Column(db.Boolean, server_default='FALSE',
nullable=False)
fetch_current_batch_end_id = db.Column(db.String)
fetch_current_batch_end_date = db.Column(db.DateTime(timezone=True))
reason = db.Column(db.String)
dormant = db.Column(db.Boolean, server_default='FALSE', nullable=False)
backoff_level = db.Column(db.Integer, server_default='0', nullable=False)
backoff_until = db.Column(db.DateTime(timezone=True), server_default='now', nullable=False)
BACKOFF_MAX = 14
# backoff is 10 seconds * 2^backoff_level
# this gives us roughly 1.8 days at level 14
def touch_fetch(self):
self.last_fetch = db.func.now()
def touch_delete(self):
self.last_delete = db.func.now()
# if it's been more than 1 delete cycle ago that we've deleted a post,
# reset next_delete to be 1 cycle away
if (datetime.now(timezone.utc) - self.next_delete
> self.policy_delete_every):
self.next_delete = db.func.now() + self.policy_delete_every
else:
self.next_delete += self.policy_delete_every
def touch_refresh(self):
self.last_refresh = db.func.now()
def get_avatar(self):
from app import imgproxy
from flask import url_for
return url_for('avatar', identifier=imgproxy.identifier_for(self.avatar_url))
@db.validates('policy_keep_younger', 'policy_delete_every')
def validate_intervals(self, key, value):
if not (value == timedelta(0) or value >= timedelta(minutes=1)):
value = timedelta(minutes=1)
if key == 'policy_delete_every' and \
self.next_delete and\
datetime.now(timezone.utc) + value < self.next_delete:
# make sure that next delete is not in the far future
self.next_delete = datetime.now(timezone.utc) + value
return value
# pylint: disable=R0201
@db.validates('policy_keep_latest')
def validate_empty_string_is_zero(self, key, value):
if type(value) == str and value.strip() == '':
if isinstance(value, str) and value.strip() == '':
return 0
return value
@db.validates('policy_enabled')
def on_policy_enable(self, key, enable):
if not self.policy_enabled and enable:
self.next_delete = (
datetime.now(timezone.utc) + self.policy_delete_every)
self.reason = None
self.dormant = False
return enable
@db.validates('policy_keep_direct')
def validate_bool_accept_string(self, key, value):
if isinstance(value, str):
return value.lower() == 'true'
return value
# backref: tokens
# backref: twitter_archives
# backref: posts
# backref: sessions
def __repr__(self):
return f"<Account({self.id}, {self.screen_name}, {self.display_name})>"
def post_count(self):
return Post.query.with_parent(self).count()
return Post.query.with_parent(self, 'posts').count()
def estimate_eligible_for_delete(self):
"""
this is an estimation because we do not know if favourite status has changed since last time a post was refreshed
and it is unfeasible to refresh every single post every time we need to know how many posts are eligible to delete
this is an estimation because we do not know if favourite status has
changed since last time a post was refreshed and it is unfeasible to
refresh every single post every time we need to know how many posts are
eligible to delete
"""
latest_n_posts = Post.query.with_parent(self).order_by(db.desc(Post.created_at)).limit(self.policy_keep_latest)
query = Post.query.with_parent(self).\
filter(Post.created_at + self.policy_keep_younger <= db.func.now()).\
except_(latest_n_posts)
if(self.policy_keep_favourites):
query = query.filter_by(favourite = False)
if(self.policy_keep_media):
query = query.filter_by(has_media = False)
latest_n_posts = (Post.query.with_parent(self, 'posts')
.order_by(db.desc(Post.created_at))
.limit(self.policy_keep_latest))
query = (Post.query.with_parent(self, 'posts')
.filter(Post.created_at <=
db.func.now() - self.policy_keep_younger)
.except_(latest_n_posts))
if(self.policy_keep_favourites != 'none'):
query = query.filter(db.or_(
Post.favourite == (self.policy_keep_favourites == 'deleteonly'),
Post.is_reblog))
if(self.policy_keep_media != 'none'):
query = query.filter(db.or_(
Post.has_media == (self.policy_keep_media == 'deleteonly'),
Post.is_reblog))
if(self.policy_keep_direct):
query = query.filter(~Post.direct)
return query.count()
def force_log_out(self):
Session.query.with_parent(self).delete()
db.session.commit()
def backoff(self):
self.backoff_level = min(self.backoff_level + 1, self.BACKOFF_MAX)
backoff_for = 10 * 2 ** self.backoff_level
backoff_for *= random.uniform(1, 1.3)
self.backoff_until = datetime.utcnow() + timedelta(seconds=backoff_for)
def reset_backoff(self):
self.backoff_until = datetime.utcnow()
self.backoff_level = 0
class Account(Account, db.Model):
pass
def __str__(self):
return f"<Account({self.id}, {self.screen_name}, {self.display_name})>"
class OAuthToken(db.Model, TimestampMixin):
__tablename__ = 'oauth_tokens'
token = db.Column(db.String, primary_key=True)
token_secret = db.Column(db.String, nullable=False)
token_secret = db.Column(db.String, nullable=True)
account_id = db.Column(db.String, db.ForeignKey('accounts.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=True)
account = db.relationship(Account, backref=db.backref('tokens', order_by=lambda: db.desc(OAuthToken.created_at)))
account_id = db.Column(db.String,
db.ForeignKey('accounts.id', ondelete='CASCADE',
onupdate='CASCADE'),
nullable=True, index=True)
account = db.relationship(
Account,
backref=db.backref('tokens',
order_by=lambda: db.desc(OAuthToken.created_at))
)
# note: account_id is nullable here because we don't know what account a token is for
# until we call /account/verify_credentials with it
# note: account_id is nullable here because we don't know what account a
# token is for until we call /account/verify_credentials with it
class Session(db.Model, TimestampMixin):
__tablename__ = 'sessions'
id = db.Column(db.String, primary_key=True, default=lambda: secrets.token_urlsafe())
id = db.Column(db.String, primary_key=True,
default=secrets.token_urlsafe)
account_id = db.Column(db.String, db.ForeignKey('accounts.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
account_id = db.Column(
db.String,
db.ForeignKey('accounts.id',
ondelete='CASCADE', onupdate='CASCADE'),
nullable=False, index=True)
account = db.relationship(Account, lazy='joined', backref='sessions')
csrf_token = db.Column(db.String,
default=secrets.token_urlsafe,
nullable=False)
class Post(db.Model, TimestampMixin, RemoteIDMixin):
__tablename__ = 'posts'
id = db.Column(db.String, primary_key=True)
body = db.Column(db.String)
author_id = db.Column(db.String, db.ForeignKey('accounts.id', ondelete='CASCADE', onupdate='CASCADE'), nullable=False)
author = db.relationship(Account,
backref=db.backref('posts', order_by=lambda: db.desc(Post.created_at)))
author_id = db.Column(
db.String,
db.ForeignKey('accounts.id',
ondelete='CASCADE', onupdate='CASCADE'),
nullable=False)
author = db.relationship(
Account,
foreign_keys = (author_id,),
backref=db.backref('posts',
order_by=lambda: db.desc(Post.created_at)))
favourite = db.Column(db.Boolean, server_default='FALSE', nullable=False)
has_media = db.Column(db.Boolean, server_default='FALSE', nullable=False)
direct = db.Column(db.Boolean, server_default='FALSE', nullable=False)
def snippet(self):
if len(self.body) > 20:
return self.body[:19] + ""
return self.body
is_reblog = db.Column(db.Boolean, server_default='FALSE', nullable=False)
def __str__(self):
return '<Post ({}, Author: {})>'.format(self.id, self.author_id)
db.Index('ix_posts_author_id_created_at', Post.author_id, Post.created_at)
def __repr__(self):
return '<Post ({}, "{}", Author: {})>'.format(self.id, self.snippet(), self.author_id)
class TwitterArchive(db.Model, TimestampMixin):
__tablename__ = 'twitter_archives'
id = db.Column(db.Integer, primary_key=True)
account_id = db.Column(db.String, db.ForeignKey('accounts.id', onupdate='CASCADE', ondelete='CASCADE'), nullable=False)
account = db.relationship(Account, backref=db.backref('twitter_archives', order_by=lambda: db.desc(TwitterArchive.id)))
account_id = db.Column(
db.String,
db.ForeignKey('accounts.id',
onupdate='CASCADE', ondelete='CASCADE'),
nullable=False)
account = db.relationship(
Account,
backref=db.backref('twitter_archives',
order_by=lambda: db.desc(TwitterArchive.id)))
body = db.deferred(db.Column(db.LargeBinary, nullable=False))
chunks = db.Column(db.Integer)
chunks_successful = db.Column(db.Integer, server_default='0', nullable=False)
chunks_successful = db.Column(db.Integer,
server_default='0', nullable=False)
chunks_failed = db.Column(db.Integer, server_default='0', nullable=False)
def status(self):
@ -170,3 +359,52 @@ class TwitterArchive(db.Model, TimestampMixin):
if self.chunks_successful == self.chunks:
return 'successful'
return 'pending'
ProtoEnum = db.Enum('http', 'https', name='enum_protocol')
class MastodonApp(db.Model, TimestampMixin):
__tablename__ = 'mastodon_apps'
instance = db.Column(db.String, primary_key=True)
client_id = db.Column(db.String, nullable=False)
client_secret = db.Column(db.String, nullable=False)
protocol = db.Column(ProtoEnum, nullable=False)
class MastodonInstance(db.Model):
"""
this is for the autocomplete in the mastodon login form
it isn't coupled with anything else so that we can seed it with
some popular instances ahead of time
"""
__tablename__ = 'mastodon_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
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

263
package-lock.json generated Normal file
View File

@ -0,0 +1,263 @@
{
"name": "forget",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"devDependencies": {
"rollup": "^2.42.4",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-svelte": "^7.1.0",
"svelte": "^3.35.0"
}
},
"node_modules/@types/node": {
"version": "12.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz",
"integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==",
"dev": true
},
"node_modules/@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/builtin-modules": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
"dev": true,
"engines": {
"node": ">=6"
}
},
"node_modules/estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"node_modules/require-relative": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
"integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=",
"dev": true
},
"node_modules/resolve": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz",
"integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==",
"dev": true,
"dependencies": {
"path-parse": "^1.0.6"
}
},
"node_modules/rollup": {
"version": "2.68.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.68.0.tgz",
"integrity": "sha512-XrMKOYK7oQcTio4wyTz466mucnd8LzkiZLozZ4Rz0zQD+HeX4nUK4B8GrTX/2EvN2/vBF/i2WnaXboPxo0JylA==",
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=10.0.0"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/rollup-plugin-node-resolve": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
"integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
"deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-node-resolve.",
"dev": true,
"dependencies": {
"@types/resolve": "0.0.8",
"builtin-modules": "^3.1.0",
"is-module": "^1.0.0",
"resolve": "^1.11.1",
"rollup-pluginutils": "^2.8.1"
},
"peerDependencies": {
"rollup": ">=1.11.0"
}
},
"node_modules/rollup-plugin-svelte": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz",
"integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==",
"dev": true,
"dependencies": {
"require-relative": "^0.8.7",
"rollup-pluginutils": "^2.8.2"
},
"engines": {
"node": ">=10"
},
"peerDependencies": {
"rollup": ">=2.0.0",
"svelte": ">=3.5.0"
}
},
"node_modules/rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dev": true,
"dependencies": {
"estree-walker": "^0.6.1"
}
},
"node_modules/svelte": {
"version": "3.46.4",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.46.4.tgz",
"integrity": "sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg==",
"dev": true,
"engines": {
"node": ">= 8"
}
}
},
"dependencies": {
"@types/node": {
"version": "12.0.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz",
"integrity": "sha512-LcsGbPomWsad6wmMNv7nBLw7YYYyfdYcz6xryKYQhx89c3XXan+8Q6AJ43G5XDIaklaVkK3mE4fCb0SBvMiPSQ==",
"dev": true
},
"@types/resolve": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz",
"integrity": "sha512-auApPaJf3NPfe18hSoJkp8EbZzer2ISk7o8mCC3M9he/a04+gbMF97NkpD2S8riMGvm4BMRI59/SZQSaLTKpsQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"builtin-modules": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.1.0.tgz",
"integrity": "sha512-k0KL0aWZuBt2lrxrcASWDfwOLMnodeQjodT/1SxEQAXsHANgo6ZC/VEaSEHCXt7aSTZ4/4H5LKa+tBXmW7Vtvw==",
"dev": true
},
"estree-walker": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
"integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
"dev": true
},
"fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"optional": true
},
"is-module": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
"integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=",
"dev": true
},
"path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"require-relative": {
"version": "0.8.7",
"resolved": "https://registry.npmjs.org/require-relative/-/require-relative-0.8.7.tgz",
"integrity": "sha1-eZlTn8ngR6N5KPoZb44VY9q9Nt4=",
"dev": true
},
"resolve": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz",
"integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
},
"rollup": {
"version": "2.68.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.68.0.tgz",
"integrity": "sha512-XrMKOYK7oQcTio4wyTz466mucnd8LzkiZLozZ4Rz0zQD+HeX4nUK4B8GrTX/2EvN2/vBF/i2WnaXboPxo0JylA==",
"dev": true,
"requires": {
"fsevents": "~2.3.2"
}
},
"rollup-plugin-node-resolve": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-5.2.0.tgz",
"integrity": "sha512-jUlyaDXts7TW2CqQ4GaO5VJ4PwwaV8VUGA7+km3n6k6xtOEacf61u0VXwN80phY/evMcaS+9eIeJ9MOyDxt5Zw==",
"dev": true,
"requires": {
"@types/resolve": "0.0.8",
"builtin-modules": "^3.1.0",
"is-module": "^1.0.0",
"resolve": "^1.11.1",
"rollup-pluginutils": "^2.8.1"
}
},
"rollup-plugin-svelte": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/rollup-plugin-svelte/-/rollup-plugin-svelte-7.1.0.tgz",
"integrity": "sha512-vopCUq3G+25sKjwF5VilIbiY6KCuMNHP1PFvx2Vr3REBNMDllKHFZN2B9jwwC+MqNc3UPKkjXnceLPEjTjXGXg==",
"dev": true,
"requires": {
"require-relative": "^0.8.7",
"rollup-pluginutils": "^2.8.2"
}
},
"rollup-pluginutils": {
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
"integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
"dev": true,
"requires": {
"estree-walker": "^0.6.1"
}
},
"svelte": {
"version": "3.46.4",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-3.46.4.tgz",
"integrity": "sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg==",
"dev": true
}
}
}

8
package.json Normal file
View File

@ -0,0 +1,8 @@
{
"devDependencies": {
"rollup": "^2.42.4",
"rollup-plugin-node-resolve": "^5.2.0",
"rollup-plugin-svelte": "^7.1.0",
"svelte": "^3.35.0"
}
}

73
requirements-dev.txt Normal file
View File

@ -0,0 +1,73 @@
#
# These requirements were autogenerated by pipenv
# To regenerate from the project's Pipfile, run:
#
# pipenv lock --requirements --dev
#
# Note: in pipenv 2020.x, "--dev" changed to emit both default and development
# requirements. To emit only development requirements, pass "--dev-only".
-i https://pypi.python.org/simple
alembic==1.7.6
amqp==5.0.9; python_version >= '3.6'
attrs==21.4.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
billiard==3.6.4.0
blinker==1.4
blurhash==1.1.4
brotli==1.0.9
celery==5.2.3
certifi==2021.10.8
charset-normalizer==2.0.12; python_version >= '3'
click-didyoumean==0.3.0; python_version < '4' and python_full_version >= '3.6.2'
click-plugins==1.1.1
click-repl==0.2.0
click==8.0.4; python_version >= '3.6'
cloudpickle==2.0.0; python_version >= '3.6'
codecov==2.1.12
coverage==6.3.2
csscompressor==0.9.5
decorator==5.1.1; python_version >= '3.5'
deprecated==1.2.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
doit==0.34.2
flask-migrate==3.1.0
flask-sqlalchemy==2.5.1
flask==2.0.3
greenlet==1.1.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))
gunicorn==20.1.0
honcho==1.1.0
idna==3.3; python_version >= '3'
iniconfig==1.1.1
itsdangerous==2.1.0; python_version >= '3.7'
jinja2==3.0.3; python_version >= '3.6'
kombu==5.2.3; python_version >= '3.7'
mako==1.1.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
markupsafe==2.1.0; python_version >= '3.7'
mastodon.py==1.5.1
packaging==21.3; python_version >= '3.6'
pillow==9.0.1
pluggy==1.0.0; python_version >= '3.6'
prompt-toolkit==3.0.28; python_full_version >= '3.6.2'
psycopg2==2.9.3
py==1.11.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pyinotify==0.9.6; sys_platform == 'linux'
pyparsing==3.0.7; python_version >= '3.6'
pytest-cov==3.0.0
pytest==7.0.1
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-magic==0.4.25; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pytz==2021.3
raven==6.10.0
redis==4.1.4
requests==2.27.1
setuptools==59.6.0; python_version >= '3.6'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlalchemy==1.4.31
tomli==2.0.1; python_version >= '3.7'
twitter==1.19.3
urllib3==1.26.8; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
versioneer==0.21
vine==5.0.0; python_version >= '3.6'
wcwidth==0.2.5
werkzeug==2.0.3; python_version >= '3.6'
wrapt==1.13.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'

View File

@ -1,36 +1,60 @@
alembic==0.9.5
amqp==2.2.1
billiard==3.5.0.3
Brotli==0.6.0
celery==4.1.0
click==6.7
cloudpickle==0.4.0
contextlib2==0.5.5
csscompressor==0.9.4
doit==0.30.3
Flask==0.12.2
Flask-Limiter==0.9.5
Flask-Migrate==2.1.0
Flask-SQLAlchemy==2.2
gunicorn==19.7.1
honcho==1.0.1
itsdangerous==0.24
Jinja2==2.9.6
kombu==4.1.0
limits==1.2.1
Mako==1.0.7
MarkupSafe==1.0
olefile==0.44
Pillow==4.2.1
psycopg2==2.7.3
pyinotify==0.9.6
python-dateutil==2.6.1
python-editor==1.0.3
pytz==2017.2
raven==6.1.0
redis==2.10.5
six==1.10.0
SQLAlchemy==1.1.13
twitter==1.17.1
vine==1.1.4
Werkzeug==0.12.2
#
# These requirements were autogenerated by pipenv
# To regenerate from the project's Pipfile, run:
#
# pipenv lock --requirements
#
-i https://pypi.python.org/simple
alembic==1.7.6
amqp==5.0.9; python_version >= '3.6'
billiard==3.6.4.0
blinker==1.4
blurhash==1.1.4
brotli==1.0.9
celery==5.2.3
certifi==2021.10.8
charset-normalizer==2.0.12; python_version >= '3'
click-didyoumean==0.3.0; python_version < '4' and python_full_version >= '3.6.2'
click-plugins==1.1.1
click-repl==0.2.0
click==8.0.4; python_version >= '3.6'
cloudpickle==2.0.0; python_version >= '3.6'
csscompressor==0.9.5
decorator==5.1.1; python_version >= '3.5'
deprecated==1.2.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
doit==0.34.2
flask-migrate==3.1.0
flask-sqlalchemy==2.5.1
flask==2.0.3
greenlet==1.1.2; python_version >= '3' and platform_machine == 'aarch64' or (platform_machine == 'ppc64le' or (platform_machine == 'x86_64' or (platform_machine == 'amd64' or (platform_machine == 'AMD64' or (platform_machine == 'win32' or platform_machine == 'WIN32')))))
gunicorn==20.1.0
honcho==1.1.0
idna==3.3; python_version >= '3'
itsdangerous==2.1.0; python_version >= '3.7'
jinja2==3.0.3; python_version >= '3.6'
kombu==5.2.3; python_version >= '3.7'
mako==1.1.6; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
markupsafe==2.1.0; python_version >= '3.7'
mastodon.py==1.5.1
packaging==21.3; python_version >= '3.6'
pillow==9.0.1
prompt-toolkit==3.0.28; python_full_version >= '3.6.2'
psycopg2==2.9.3
pyinotify==0.9.6; sys_platform == 'linux'
pyparsing==3.0.7; python_version >= '3.6'
python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
python-magic==0.4.25; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'
pytz==2021.3
raven==6.10.0
redis==4.1.4
requests==2.27.1
setuptools==59.6.0; python_version >= '3.6'
six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'
sqlalchemy==1.4.31
twitter==1.19.3
urllib3==1.26.8; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'
vine==5.0.0; python_version >= '3.6'
wcwidth==0.2.5
werkzeug==2.0.3; python_version >= '3.6'
wrapt==1.13.3; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'

17
rollup.config.js Normal file
View File

@ -0,0 +1,17 @@
import svelte from 'rollup-plugin-svelte';
import node_resolve from 'rollup-plugin-node-resolve';
export default {
output: {
format: 'iife',
},
plugins: [
svelte({
extensions: ['.html'],
include: 'components/**/*.html',
compilerOptions: {hydratable: true},
emitCss: false,
}),
node_resolve(),
]
}

182
routes.py
View File

@ -1,182 +0,0 @@
from flask import render_template, url_for, redirect, request, g, Response, jsonify
from datetime import datetime, timedelta
import lib.twitter
import lib
from lib.auth import require_auth, require_auth_api
from lib import set_session_cookie
from lib import get_viewer_session, get_viewer
from model import Account, Session, Post, TwitterArchive
from app import app, db, sentry, limiter
import tasks
from zipfile import BadZipFile
from twitter import TwitterError
from urllib.error import URLError
import version
import lib.brotli
import lib.settings
@app.before_request
def load_viewer():
g.viewer = get_viewer_session()
if g.viewer and sentry:
sentry.user_context({
'id': g.viewer.account.id,
'username': g.viewer.account.screen_name,
'service': g.viewer.account.service
})
@app.context_processor
def inject_version():
return dict(version=version.version)
@app.context_processor
def inject_sentry():
if sentry:
client_dsn = app.config.get('SENTRY_DSN').split('@')
client_dsn[:1] = client_dsn[0].split(':')
client_dsn = ':'.join(client_dsn[0:2]) + '@' + client_dsn[3]
return dict(sentry_dsn=client_dsn)
return dict()
@app.after_request
def touch_viewer(resp):
if 'viewer' in g and g.viewer:
set_session_cookie(g.viewer, resp, app.config.get('HTTPS'))
g.viewer.touch()
db.session.commit()
return resp
lib.brotli.brotli(app)
@app.route('/')
def index():
if g.viewer:
return render_template('logged_in.html', scales=lib.interval_scales,
tweet_archive_failed = 'tweet_archive_failed' in request.args,
settings_error = 'settings_error' in request.args
)
else:
return render_template('index.html',
twitter_login_error = 'twitter_login_error' in request.args)
@app.route('/login/twitter')
@limiter.limit('3/minute')
def twitter_login_step1():
try:
return redirect(lib.twitter.get_login_url(
callback = url_for('twitter_login_step2', _external=True),
**app.config.get_namespace("TWITTER_")
))
except (TwitterError, URLError):
return redirect(url_for('index', twitter_login_error='', _anchor='log_in'))
@app.route('/login/twitter/callback')
@limiter.limit('3/minute')
def twitter_login_step2():
try:
oauth_token = request.args['oauth_token']
oauth_verifier = request.args['oauth_verifier']
token = lib.twitter.receive_verifier(oauth_token, oauth_verifier, **app.config.get_namespace("TWITTER_"))
session = Session(account_id = token.account_id)
db.session.add(session)
db.session.commit()
tasks.fetch_acc.s(token.account_id).apply_async(routing_key='high')
resp = Response(status=302, headers={"location": url_for('index')})
set_session_cookie(session, resp, app.config.get('HTTPS'))
return resp
except (TwitterError, URLError):
return redirect(url_for('index', twitter_login_error='', _anchor='log_in'))
@app.route('/upload_tweet_archive', methods=('POST',))
@limiter.limit('10/10 minutes')
@require_auth
def upload_tweet_archive():
ta = TwitterArchive(account = g.viewer.account,
body = request.files['file'].read())
db.session.add(ta)
db.session.commit()
try:
tasks.chunk_twitter_archive(ta.id)
assert ta.chunks > 0
return redirect(url_for('index', _anchor='recent_archives'))
except (BadZipFile, AssertionError):
return redirect(url_for('index', tweet_archive_failed='', _anchor='tweet_archive_import'))
@app.route('/settings', methods=('POST',))
@require_auth
def settings():
for attr in lib.settings.attrs:
try:
if attr in request.form:
setattr(g.viewer.account, attr, request.form[attr])
except ValueError:
return redirect(url_for('index', settings_error=''))
db.session.commit()
return redirect(url_for('index', settings_saved=''))
@app.route('/disable', methods=('POST',))
@require_auth
def disable():
g.viewer.account.policy_enabled = False
db.session.commit()
return redirect(url_for('index'))
@app.route('/enable', methods=('POST',))
@require_auth
def enable():
risky = False
if not 'confirm' in request.form and not g.viewer.account.policy_enabled:
if g.viewer.account.policy_delete_every == timedelta(0):
approx = g.viewer.account.estimate_eligible_for_delete()
return render_template('warn.html', message=f"""You've set the time between deleting posts to 0. Every post that matches your expiration rules will be deleted within minutes.
{ ("That's about " + str(approx) + " posts.") if approx > 0 else "" }
Go ahead?""")
if g.viewer.account.last_delete < datetime.now() - timedelta(days=365):
return render_template('warn.html', message="""Once you enable Forget, posts that match your expiration rules will be deleted <b>permanently</b>. We can't bring them back. Make sure that you won't miss them.""")
if not g.viewer.account.policy_enabled:
g.viewer.account.last_delete = db.func.now()
g.viewer.account.policy_enabled = True
db.session.commit()
return redirect(url_for('index'))
@app.route('/logout')
@require_auth
def logout():
if(g.viewer):
db.session.delete(g.viewer)
db.session.commit()
g.viewer = None
return redirect(url_for('index'))
@app.route('/api/about')
def api_about():
return jsonify(service='Forget', version=version.version)
@app.route('/api/settings', methods=('PUT',))
@require_auth_api
def api_settings_put():
viewer = get_viewer()
data = request.json
updated = dict()
for key in lib.settings.attrs:
if key in data:
setattr(viewer, key, data[key])
updated[key] = data[key]
db.session.commit()
return jsonify(status='success', updated=updated)

333
routes/__init__.py Normal file
View File

@ -0,0 +1,333 @@
from flask import render_template, url_for, redirect, request, g,\
make_response
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 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
import libforget.json
import re
@app.route('/')
def index():
viewer = get_viewer()
if viewer:
return render_template(
'logged_in.html',
scales=libforget.interval.SCALES,
tweet_archive_failed='tweet_archive_failed' in request.args,
settings_error='settings_error' in request.args,
viewer_json=libforget.json.account(viewer),
)
else:
return redirect(url_for('about'))
@app.route('/about/')
def about():
blocklist = app.config.get('HIDDEN_INSTANCES', '').split()
mastodon_instances = libforget.mastodon.suggested_instances(blocklist=blocklist)
misskey_instances = libforget.misskey.suggested_instances(blocklist=blocklist)
return render_template(
'about.html',
mastodon_instances=mastodon_instances,
misskey_instances=misskey_instances,
twitter_login_error='twitter_login_error' in request.args)
@app.route('/about/privacy')
def privacy():
return render_template('privacy.html')
@app.route('/login/twitter')
def twitter_login_step1():
try:
return redirect(libforget.twitter.get_login_url(
callback=url_for('twitter_login_step2', _external=True),
**app.config.get_namespace("TWITTER_")
))
except (TwitterError, URLError):
if sentry:
sentry.captureException()
return redirect(
url_for('about', twitter_login_error='', _anchor='log_in'))
def login(account_id):
session = Session(account_id=account_id)
db.session.add(session)
db.session.commit()
session.account.dormant = False
db.session.commit()
tasks.fetch_acc.s(account_id).apply_async(routing_key='high')
return session
@app.route('/login/twitter/callback')
def twitter_login_step2():
try:
oauth_token = request.args.get('oauth_token', '')
oauth_verifier = request.args.get('oauth_verifier', '')
token = libforget.twitter.receive_verifier(
oauth_token, oauth_verifier,
**app.config.get_namespace("TWITTER_"))
session = login(token.account_id)
g.viewer = session
return redirect(url_for('index'))
except Exception:
if sentry:
sentry.captureException()
return redirect(
url_for('about', twitter_login_error='', _anchor='log_in'))
@app.route('/upload_tweet_archive', methods=('POST',))
def upload_tweet_archive():
return 403, 'Tweet archive support is temporarily disabled, see banner on the front page.'
@app.route('/settings', methods=('POST',))
@csrf
@require_auth
def settings():
viewer = get_viewer()
try:
for attr in libforget.settings.attrs:
if attr in request.form:
setattr(viewer, attr, request.form[attr])
db.session.commit()
except ValueError:
if sentry:
sentry.captureException()
return 400
return redirect(url_for('index', settings_saved=''))
@app.route('/disable', methods=('POST',))
@csrf
@require_auth
def disable():
g.viewer.account.policy_enabled = False
db.session.commit()
return redirect(url_for('index'))
@app.route('/enable', methods=('POST',))
@csrf
@require_auth
def enable():
if 'confirm' not in request.form and not g.viewer.account.policy_enabled:
if g.viewer.account.policy_delete_every == timedelta(0):
approx = g.viewer.account.estimate_eligible_for_delete()
return render_template(
'warn.html',
message=f"""
You've set the time between deleting posts to 0. Every post
that matches your expiration rules will be deleted within
minutes.
{ ("That's about " + str(approx) + " posts.") if approx > 0
else "" }
Go ahead?
""")
if (not g.viewer.account.last_delete or
g.viewer.account.last_delete <
datetime.now(timezone.utc) - timedelta(days=365)):
return render_template(
'warn.html',
message="""
Once you enable Forget, posts that match your
expiration rules will be deleted <b>permanently</b>.
We can't bring them back. Make sure that you won't
miss them.
""")
g.viewer.account.policy_enabled = True
db.session.commit()
return redirect(url_for('index'))
@app.route('/logout')
@require_auth
def logout():
if(g.viewer):
db.session.delete(g.viewer)
db.session.commit()
g.viewer = None
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):
instance_url = (request.args.get('instance_url', None)
or request.form.get('instance_url', None))
if not instance_url:
blocklist = app.config.get('HIDDEN_INSTANCES', '').split()
instances = libforget.mastodon.suggested_instances(
limit=30,
min_popularity=1,
blocklist=blocklist,
)
return render_template(
'mastodon_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('mastodon_login_step2',
instance_url=instance_url, _external=True)
try:
mastoapp = libforget.mastodon.get_or_create_app(
instance_url,
callback,
url_for('index', _external=True))
db.session.merge(mastoapp)
db.session.commit()
return redirect(libforget.mastodon.login_url(mastoapp, callback))
except Exception:
if sentry:
sentry.captureException()
return redirect(url_for('mastodon_login_step1', error=True))
@app.route('/login/mastodon/callback/<instance_url>')
def mastodon_login_step2(instance_url):
code = request.args.get('code', None)
mastoapp = MastodonApp.query.get(instance_url)
if not code or not mastoapp:
return redirect(url_for('mastodon_login_step1', error=True))
callback = url_for('mastodon_login_step2',
instance_url=instance_url, _external=True)
token = libforget.mastodon.receive_code(code, mastoapp, callback)
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('/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:
blocklist = app.config.get('HIDDEN_INSTANCES', '').split()
instances = libforget.misskey.suggested_instances(
limit = 30,
min_popularity = 1,
blocklist=blocklist,
)
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()
mkapp = libforget.misskey.get_or_create_app(
instance_url,
callback,
url_for('index', _external=True),
session)
db.session.merge(mkapp)
db.session.commit()
return redirect(libforget.misskey.login_url(mkapp, 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)
mkapp = MisskeyApp.query.get(instance_url)
if not token or not mkapp:
return redirect(url_for('misskey_login', error=True))
token = libforget.misskey.receive_token(token, mkapp)
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('@')
client_dsn[:1] = client_dsn[0].split(':')
client_dsn = ':'.join(client_dsn[0:2]) + '@' + client_dsn[3]
resp = make_response(render_template(
'sentry.js', sentry_dsn=client_dsn))
resp.headers.set('content-type', 'text/javascript')
resp.headers.set('cache-control', 'public; max-age=3600')
return resp
@app.route('/dismiss', methods={'POST'})
@csrf
@require_auth
def dismiss():
get_viewer().reason = None
db.session.commit()
return redirect(url_for('index'))
@app.route('/avatar/<identifier>')
def avatar(identifier):
return imgproxy.respond(identifier)

69
routes/api.py Normal file
View File

@ -0,0 +1,69 @@
from app import app, db, imgproxy
from libforget.auth import require_auth_api, get_viewer
from flask import jsonify, redirect, make_response, request, Response
from model import Account
import libforget.settings
import libforget.json
import random
@app.route('/api/health_check') # deprecated 2021-03-12
@app.route('/api/status_check')
def api_status_check():
try:
db.session.execute('SELECT 1')
except Exception:
return ('PostgreSQL bad', 500)
try:
imgproxy.redis.set('forget-status-check', 'howdy', ex=5)
except Exception:
return ('Redis bad', 500)
return 'OK'
@app.route('/api/settings', methods=('PUT',))
@require_auth_api
def api_settings_put():
viewer = get_viewer()
data = request.json
updated = dict()
for key in libforget.settings.attrs:
if key in data:
if (
isinstance(getattr(viewer, key), bool) and
isinstance(data[key], str)):
data[key] = data[key] == 'true'
setattr(viewer, key, data[key])
updated[key] = data[key]
db.session.commit()
return jsonify(status='success', updated=updated)
@app.route('/api/viewer')
@require_auth_api
def api_viewer():
viewer = get_viewer()
resp = make_response(libforget.json.account(viewer))
resp.headers.set('content-type', 'application/json')
return resp
@app.route('/api/reason', methods={'DELETE'})
@require_auth_api
def delete_reason():
get_viewer().reason = None
db.session.commit()
return jsonify(status='success')
@app.route('/api/badge/users')
def users_badge():
count = (
Account.query.filter(Account.policy_enabled)
.filter(~Account.dormant)
.count()
)
return redirect(
"https://img.shields.io/badge/active%20users-{}-blue.svg"
.format(count))

65
routes/misc.py Normal file
View File

@ -0,0 +1,65 @@
from app import app, db, sentry
from flask import g, render_template, make_response, redirect, request
import version
import libforget.version
from libforget.auth import get_viewer_session, set_session_cookie
@app.before_request
def load_viewer():
g.viewer = get_viewer_session()
if g.viewer and sentry:
sentry.user_context({
'id': g.viewer.account.id,
'username': g.viewer.account.screen_name,
'service': g.viewer.account.service
})
@app.context_processor
def inject_version():
v = version.get_versions()
return dict(
version=v['version'],
repo_url=libforget.version.url_for_version(v),
)
@app.context_processor
def inject_sentry():
if sentry:
return dict(sentry=True)
return dict()
@app.after_request
def touch_viewer(resp):
if 'viewer' in g and g.viewer:
set_session_cookie(g.viewer, resp, app.config.get('HTTPS'))
g.viewer.touch()
db.session.commit()
return resp
@app.errorhandler(404)
def not_found(e):
return (render_template('404.html', e=e), 404)
@app.errorhandler(500)
def internal_server_error(e):
if request.endpoint and request.endpoint.startswith('api_'):
return e.get_response()
return (render_template('500.html', e=e), 500)
@app.route('/robots.txt')
def robotstxt():
resp = make_response('')
resp.headers.set('content-type', 'text/plain')
return resp
@app.route('/humans.txt')
def humanstxt():
return redirect('https://github.com/codl/forget/graph/contributors')

8
setup.cfg Normal file
View File

@ -0,0 +1,8 @@
[versioneer]
VCS = git
style = pep440
versionfile_source = version.py
versionfile_build = version.py
tag_prefix = v
#parentdir_prefix =

16
setup.py Normal file
View File

@ -0,0 +1,16 @@
from setuptools import setup
import versioneer
setup(
name='forget',
description='A post-expiring service.',
url='https://forget.codl.fr/',
author='codl',
author_email='codl@codl.fr',
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
)

Some files were not shown because too many files have changed in this diff Show More