diff --git a/.gitignore b/.gitignore index f85c6b1..c8f9ffc 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -config.py \ No newline at end of file +config.py +config.yaml \ No newline at end of file diff --git a/README.md b/README.md index 6c1455f..92ab249 100644 --- a/README.md +++ b/README.md @@ -1,90 +1,160 @@ -A script for deleting old toots. +A tool for deleting old toots, written in Python 3. -Based partially on [tweet-deleting script](https://gist.github.com/flesueur/bcb2d9185b64c5191915d860ad19f23f) by [@flesueur](https://github.com/flesueur) +# Prior work +The initial `ephemetoot` script was based on [this tweet-deleting script](https://gist.github.com/flesueur/bcb2d9185b64c5191915d860ad19f23f) by [@flesueur](https://github.com/flesueur) + +`ephemetoot` relies heavily on the Mastodon.py package by [@halcy](https://github.com/halcy) # Usage -You can use this script to delete [Mastodon](https://github.com/tootsuite/mastodon) toots that are older than a certain number of days. By default it will keep any pinned toots, but if you want them to be deleted as well you can change `keep_pinned` to `False` in `config.py`. You can also make a list toots that you want to keep, by adding the ID numbers to the `toots_to_keep` list in `config.py` (see point 9 below). The ID of a toot is the last part of its individual URL. e.g. for [https://ausglam.space/@hugh/101294246770105799](https://ausglam.space/@hugh/101294246770105799) the id is `101294246770105799` - -This script requires Python3, the `mastodon.py` package and an API access token. +You can use `ephemetoot` to delete [Mastodon](https://github.com/tootsuite/mastodon) toots that are older than a certain number of days. Toots can optionally be saved from deletion if: +* they are pinned; +* they include certain hashtags; +* they have certain visibility; or +* they are individually listed to be kept # Setup -1. Install Python3 if you don't already have it (recommended approach is to [use Homebrew](https://docs.brew.sh/Homebrew-and-Python) if you're on MacOS) -2. Install the mastodon package: `pip3 install mastodon.py` -3. Copy _example.config.py_ to a new file called _config.py_ (e.g. `cp example.config.py config.py`) -4. Log in to your Mastodon account using a web browser - 1. Click the settings cog - 2. Click on Development - 3. Click 'NEW APPLICATION' - 4. Enter an application name, and give the app 'read' and 'write' Scopes - 5. Click 'SUBMIT' - 6. Click on the name of the new app - 7. Copy the 'access token' string -5. Replace `YOUR_ACCESS_TOKEN_HERE` in config.py with the access token string -6. Set the `base_url` to match your mastodon server -7. Set the `days_to_keep` to the number of days you want to keep toots before deleting them -8. If you do **not** wish to keep all pinned toots regardless of age, change `keep_pinned` to `False` -9. If there are any other toots you want to keep, put the ID numbers (without quotes) in the `toots_to_keep` list, separated by commas. For example: -```python -toots_to_keep = [100029521330725397, 100013562864734780, 100044187305250752] +## Install Python 3 + +You need to [install Python 3](https://wiki.python.org/moin/BeginnersGuide/Download) to use `ephemetoot`. Python 2 is now end-of-life, however it continued to be installed as the default Python on MacOS until very recently, and may also be installed on your server. + +## Install ephemetoot +### get code with git +If you already have `git` installed on the machine where you're running ephemetoot, you can download the latest release with: +```shell +git clone https://github.com/hughrun/ephemetoot.git ``` -10. If you want to keep toots with a particular hashtag, list each hashtag in the `hashtags_to_keep` set (omitting the `#`): -```python -hashtags_to_keep = {'introduction', 'announcement'} +### get code by downloading zip file +If you don't have `git` or don't want to use it, you can download the zip file by clicking the green `Clone or download` button above and selecting `Download ZIP`. You will then need to unzip the file into a new directory where you want to run it. + +### install using pip +From a command line, move into the main `ephemetoot` directory (i.e. where the README file is) and run: +```shell +pip install . ``` -11. You can keep toots with particular visibility (e.g. direct messages) by including that visibility in `visibility_to_keep`. For example the following would only delete public toots: -```python -visibility_to_keep = ['unlisted', 'private', 'direct'] +With some Python 3 installations (e.g on MacOS with Homebrew) you may need to use: +```shell +pip3 install . ``` +## Obtain an access token + +Now you've installed `ephemetoot`, in order to actually use it you will need an application "access token" from each user. Log in to your Mastodon account using a web browser: + +1. Click the `settings` cog +2. Click on `Development` +3. Click `NEW APPLICATION` +4. Enter an application name (e.g. 'ephemetoot'), and give the app both 'read' and 'write' Scopes +5. Click `SUBMIT` +6. Click on the name of the new app, which should be a link +7. Copy the `Your access token` string + +## Configuration file + +As of version 2, you can use a single `ephemetoot` installation to delete toots from multiple accounts. Configuration for each user is set up in the `config.yaml` file. This uses [yaml syntax](https://yaml.org/spec/1.2/spec.html) and can be updated at any time without having to reload `ephemetoot`. + +Copy `example-config.yaml` to a new file called `config.yml`: +```shell +cp example-config.yam config.yaml +``` +You can now enter the configuration details for each user: + +| setting | description | +| ---: | :--- | +| access_token | The alphanumeric access token string from the app you created in Mastodon | +| username | Your username without the '@' or server domain. e.g. `hugh`| +| base_url | The base url of your Mastodon server, without the 'https://'. e.g. `ausglam.space`| +| days_to_keep | Number of days to keep toots e.g. `30`| +| keep_pinned | Either `True` or `False` - if `True`, any pinned toots will be kept regardless of age | +| toots_to_keep | A list of toot ids indicating toots to be kept regardless of other settings. The ID of a toot is the last part of its individual URL. e.g. for [https://ausglam.space/@hugh/101294246770105799](https://ausglam.space/@hugh/101294246770105799) the id is `101294246770105799` | +| hashtags_to_keep | a Set of hashtags, where any toots with any of these hashtags will be kept regardless of age. Do not include the '#' symbol, and remember the [rules for hashtags](https://docs.joinmastodon.org/user/posting/#hashtags) | +| visibility_to_keep | Any toots with visibility settings in this list will be kept regardless of age. Options are: `public`, `unlisted`, `private`, `direct`. For example the following would only delete public toots: +```yaml +- unlisted +- private +- direct +``` +| + +If you want to use `ephemetoot` for multiple accounts, separate the config for each user with a single dash (`-`), as shown in the example file. + # Running the script -## Test mode +It is **strongly recommended** that you do a [test run](#running-in-test-mode) before using `ephemetoot` live. + +To call the script you can simply enter: +```shell +ephemetoot +``` + +Depending on how many toots you have and how long you want to keep them, it may take a minute or two before you see any results. + +## Specifying the config location + +By default ephemetoot expects there to be a config file called `config.yaml` in the directory from where you run the `ephemetoot` command. If you want to call it from elsewhere (e.g. from `cron`), you need to specify where your config file is: + +```shell +ephemetoot --config 'directory/config.yaml' +``` + +## Running in test mode To do a test-run without actually deleting anything, run the script with the `--test` flag: ```shell -python3 ephemetoot.py --test +ephemetoot --test ``` -Depending on how many toots you have and how long you want to keep them, it may take a minute or two before you see any results. -## Live mode +## Other flag options -Run the script with no flags: +You can use both flags together: ```shell -python3 ephemetoot.py +ephemetoot --config 'directory/config.yaml' --test +``` +Use them in any order: +```shell +ephemetoot --test --config 'directory/config.yaml' +``` +Instead of coming back to this page when you forget the flags, you can just use the help option: +```shell +ephemetoot --help ``` -Depending on how many toots you have and how long you want to keep them, it may take a minute or two before you see any results. +## Rate limits + +As of v2.7.2 the Mastodon API has a rate limit of 30 deletions per 30 minutes. `mastodon.py` automatically handles this. If you are running `ephemetoot` for the first time and/or have a lot of toots to delete, it may take a while as the script will pause when it hits a rate limit, until the required time has expired. Note that the rate limit is per access token, so using ephemetoot for multiple accounts on the same server shouldn't be a big problem, however one new user may delay action on subsequent accounts in the config file. ## Scheduling Deleting old toots daily is the best approach to keeping your timeline clean and avoiding problems wiht the API rate limit. -To run automatically every day you could try using crontab: +To run automatically every day on a n*x server you could try using crontab: 1. `crontab -e` - 2. `@daily python3 ~/ephemetoot/ephemetoot.py` + 2. `@daily ephemetoot` -Alternatively on MacOS you could use [launchd](https://www.launchd.info/) or Automator. +Alternatively on MacOS you could use [launchd](https://www.launchd.info/). Some further work on an example setup for launchd is coming soonish. -## Rate limits - -As of v2.7.2 the Mastodon API has a rate limit of 30 deletions per 30 minutes. `mastodon.py` automatically handles this. If you are running `ephemetoot` for the first time and/or have a lot of toots to delete, it may take a while as the script will pause when it hits a rate limit, until the required time has expired. - -## ASCII / utf-8 errors +# ASCII / utf-8 errors Prior to Python 3.7, running a Python script on some BSD and Linux systems may throw an error. This can be resolved by: * setting a _locale_ that encodes utf-8, by using the environment setting `PYTHONIOENCODING=utf-8` when running the script, or * upgrading your Python version to 3.7 or higher. See [Issue 11](https://github.com/hughrun/ephemetoot/issues/11) for more information. +# Uninstalling + +Uninstall using pip; +``` +pip uninstall ephemetoot +``` + # Bugs and suggestions Please check existing [issues](https://github.com/hughrun/ephemetoot/issues) and if your issue is not already listed, create a new one with as much detail as possible (but don't include your access token!). # Contributing -Contributions are very welcome, but if you want to suggest any changes or improvements, please log an issue or have a chat to [me on Mastodon](https://ausglam.space/@hugh) _before_ lodging a pull request. +Contributions are very welcome, but if you want to suggest any changes or improvements, please log an issue or have a chat to [me on Mastodon](https://ausglam.space/@hugh) _before_ making a pull request. # License diff --git a/bin/ephemetoot b/bin/ephemetoot new file mode 100644 index 0000000..448ecbf --- /dev/null +++ b/bin/ephemetoot @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +# ##################################################################### +# Ephemetoot - A script to delete your old toots +# Copyright (C) 2018 Hugh Rundle, 2019-2020 Hugh Rundle & Mark Eaton +# Initial work based on tweet-deleting script by @flesueur +# (https://gist.github.com/flesueur/bcb2d9185b64c5191915d860ad19f23f) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# You can contact Hugh on Mastodon @hugh@ausglam.space +# or email hugh [at] hughrundle [dot] net +# ##################################################################### + +# import +import yaml + +# from standard library +from argparse import ArgumentParser +import os + +# local files +from lib import ephemetoot + +parser = ArgumentParser() +parser.add_argument( + "--config", action="store", metavar="'filepath'", default="config.yaml", help="filepath of your config file, relative to the current directory. If no --config path is provided, ephemetoot will use 'config.yaml'." +) +parser.add_argument( + "--test", action="store_true", help="do a test run without deleting any toots" +) + +options = parser.parse_args() +if options.config[0] == '~': + config_file = os.path.expanduser(options.config) +elif options.config[0] == '/': + config_file = options.config +else: + config_file = os.path.join( os.getcwd(), options.config ) + +if __name__ == "__main__": + with open(config_file) as config: + for accounts in yaml.safe_load_all(config): + for user in accounts: + ephemetoot.checkToots(user, options) diff --git a/example-config.yaml b/example-config.yaml new file mode 100644 index 0000000..9e651d6 --- /dev/null +++ b/example-config.yaml @@ -0,0 +1,42 @@ +# access_token : the access token from the app you created in Mastodon at Settings - Development +# username : your username without the '@' or server domain. +# base_url : the base url of your Mastodon server, without the 'https://' +# days_to_keep : number of days to keep toots. +# keep_pinned : either True or False - if True, any pinned toots will be kept +# toots_to_keep : a List of toot ids indicating toots to be kept regardless of other settings +# hashtags_to_keep : a Set of hashtags, where any toots with any of these hashtags will be kept. Do not include the '#' symbol +# visibility_to_keep : any toots with visibility settings in this list will be kept. Options are: 'public', 'unlisted', 'private', 'direct' + +# you can list only one user, or multiple users +# each user account should be preceded by a single dash, and indented, as per below +- +# ausglam.space account + access_token : ZA-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL0 + username : alice + base_url : ausglam.space + days_to_keep : 14 + keep_pinned : True + toots_to_keep : + - 103996285277439262 + - 103976473612749097 + - 103877521458738491 + hashtags_to_keep : !!set { python, glamblogclub } + visibility_to_keep : + - direct + - private +- +# aus.social account + access_token : AZ-Yj3aBD8U8Cm7lKUp-lm9O9BmDgdhHzDeqsY8tlL9 + username : bob + base_url : aus.social + days_to_keep : 30 + keep_pinned : False + # toots_to_keep can be empty + toots_to_keep : + - + # hashtags_to_keep can be empty + hashtags_to_keep : !!set { } + # visibility_to_keep can be empty + visibility_to_keep : + - + \ No newline at end of file diff --git a/example.config.py b/example.config.py deleted file mode 100644 index 5eaff47..0000000 --- a/example.config.py +++ /dev/null @@ -1,7 +0,0 @@ -access_token = 'YOUR_ACCESS_TOKEN_HERE' -base_url = 'https://ausglam.space' -days_to_keep = 30 -keep_pinned = True -toots_to_keep = [] -hashtags_to_keep = {'introduction', 'announcement'} # comma separated Set as strings, e.g. 'introduction' -visibility_to_keep = ['private', 'unlisted'] # options are: 'public', 'unlisted', 'private', 'direct' diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ephemetoot.py b/lib/ephemetoot.py similarity index 63% rename from ephemetoot.py rename to lib/ephemetoot.py index 4b92035..d9dde13 100644 --- a/ephemetoot.py +++ b/lib/ephemetoot.py @@ -1,68 +1,38 @@ -# ##################################################################### -# Ephemetoot - A script to delete your old toots -# Copyright (C) 2018, 2020 Hugh Rundle, 2019 Hugh Rundle & Mark Eaton -# Based partially on tweet-deleting script by @flesueur -# (https://gist.github.com/flesueur/bcb2d9185b64c5191915d860ad19f23f) -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# You can contact Hugh on Mastodon @hugh@ausglam.space -# or email hugh [at] hughrundle [dot] net -# ##################################################################### - -from argparse import ArgumentParser -import config import json from mastodon import Mastodon, MastodonError from datetime import datetime, timedelta, timezone import time -parser = ArgumentParser() -parser.add_argument( - "--test", action="store_true", help="do a test run without deleting any toots" -) -options = parser.parse_args() -if options.test: - print("This is a test run...") +def checkToots(config, options, deleted_count=0): + if options.test: + print("This is a test run...") + print("Fetching account details for @" + config['username'] + "@" + config['base_url'] + "...") + mastodon = Mastodon( + access_token=config['access_token'], + api_base_url="https://" + config['base_url'], + ratelimit_method="wait", + ) -print("Fetching account details...") + cutoff_date = datetime.now(timezone.utc) - timedelta(days=config['days_to_keep']) + user_id = mastodon.account_verify_credentials().id + account = mastodon.account(user_id) + timeline = mastodon.account_statuses(user_id, limit=40) -mastodon = Mastodon( - access_token=config.access_token, - api_base_url=config.base_url, - ratelimit_method="wait", -) + print("Checking " + str(account.statuses_count) + " toots...") -cutoff_date = datetime.now(timezone.utc) - timedelta(days=config.days_to_keep) -user_id = mastodon.account_verify_credentials().id -timeline = mastodon.account_statuses(user_id, limit=40) - - -def checkToots(timeline, deleted_count=0): for toot in timeline: toot_tags = set() for tag in toot.tags: toot_tags.add(tag.name) try: - if config.keep_pinned and hasattr(toot, "pinned") and toot.pinned: + if config['keep_pinned'] and hasattr(toot, "pinned") and toot.pinned: print("📌 skipping pinned toot - " + str(toot.id)) - elif toot.id in config.toots_to_keep: + elif toot.id in config['toots_to_keep']: print("💾 skipping saved toot - " + str(toot.id)) - elif toot.visibility in config.visibility_to_keep: + elif toot.visibility in config['visibility_to_keep']: print("👀 skipping " + toot.visibility + " toot - " + str(toot.id)) - elif len(config.hashtags_to_keep.intersection(toot_tags)) > 0: + elif len(config['hashtags_to_keep'].intersection(toot_tags)) > 0: print("#️⃣ skipping toot with hashtag - " + str(toot.id)) elif cutoff_date > toot.created_at: if hasattr(toot, "reblog") and toot.reblog: @@ -137,11 +107,4 @@ def checkToots(timeline, deleted_count=0): else: print("Removed " + str(deleted_count) + " toots.") except IndexError: - print("No toots found!") - - -# trigger from here -if __name__ == "__main__": - account = mastodon.account(user_id) - print("Checking " + str(account.statuses_count) + " toots...") - checkToots(timeline) + print("No toots found!") \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..4a17b17 --- /dev/null +++ b/setup.py @@ -0,0 +1,19 @@ +from setuptools import setup, find_packages + +setup(name='ephemetoot', + version='2.0.0', + url='https://github.com/hughrun/ephemetoot', + license='GPL-3.0-or-later', + packages=find_packages(), + scripts=['bin/ephemetoot'], + install_requires=['Mastodon.py', 'pyyaml'], + zip_safe=False, + author='Hugh Rundle', + author_email='hugh@hughrundle.net', + description='A command line tool for selectively deleting old toots.', + keywords='mastodon, mastodon api', + project_urls={ + 'Source Code': 'https://github.com/hughrun/ephemetoot', + 'Documentation': 'https://github.com/hughrun/ephemetoot' + } +) \ No newline at end of file