diff --git a/.gitignore b/.gitignore index 5d381cc..2edcab6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -# ---> Python # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -21,6 +20,7 @@ parts/ sdist/ var/ wheels/ +pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg @@ -50,7 +50,6 @@ coverage.xml *.py,cover .hypothesis/ .pytest_cache/ -cover/ # Translations *.mo @@ -73,7 +72,6 @@ instance/ docs/_build/ # PyBuilder -.pybuilder/ target/ # Jupyter Notebook @@ -84,9 +82,7 @@ profile_default/ ipython_config.py # pyenv -# For a library or package, you might want to ignore these files since the code is -# intended to run in multiple environments; otherwise, check them in: -# .python-version +.python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. @@ -95,22 +91,7 @@ ipython_config.py # install all needed dependencies. #Pipfile.lock -# poetry -# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. -# This is especially recommended for binary packages to ensure reproducibility, and is more -# commonly ignored for libraries. -# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control -#poetry.lock - -# pdm -# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. -#pdm.lock -# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it -# in version control. -# https://pdm.fming.dev/#use-with-ide -.pdm.toml - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +# PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff @@ -122,6 +103,7 @@ celerybeat.pid # Environments .env +*.env .venv env/ venv/ @@ -146,17 +128,3 @@ dmypy.json # Pyre type checker .pyre/ - -# pytype static type analyzer -.pytype/ - -# Cython debug symbols -cython_debug/ - -# PyCharm -# JetBrains specific template is maintained in a separate JetBrains.gitignore that can -# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore -# and can be added to the global gitignore or merged into this file. For a more nuclear -# option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ - diff --git a/example.env b/example.env new file mode 100644 index 0000000..bdb4f01 --- /dev/null +++ b/example.env @@ -0,0 +1,5 @@ +# +# User name (email address) and password for mastodon server +MD_USER="toota-palooza@example.com" +MD_PASS="my-super-good-password" +MD_HOST="https://always.grumpy.world" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..45a6522 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "toota-palooza" +version = "1.0" +authors = [{ name="Paco Hope", email="toota-palooza@filter.paco.to" } ] +description = "Fill a mastodon public timeline with public toots from bots who post gutenberg open texts." +readme = "README.md" +license = { file="LICENSE" } +requires-python = ">=3.9" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://git.paco.to/nick/toota-palooza" +"Bug Tracker" = "https://git.paco.to/nick/toota-palooza/issues" + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project.scripts] +toota-palooza = "toota-palooza.cli:main" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8a7c86e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +build +Mastodon.py +python-dotenv +wheel +pip +python-daemon diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..fd25b2b --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[metadata] +name = toota-palooza +version = 1.0 + +[options] +packages = toota-palooza diff --git a/toota-palooza/__init__.py b/toota-palooza/__init__.py new file mode 100644 index 0000000..f4e2c24 --- /dev/null +++ b/toota-palooza/__init__.py @@ -0,0 +1,11 @@ +from mastodon import Mastodon + +''' +Mastodon.create_app( + 'toota-palooza', + api_base_url = 'https://infosec.exchange', + to_file = '../.toota-palooza.env' +) +''' + + diff --git a/toota-palooza/cli.py b/toota-palooza/cli.py new file mode 100644 index 0000000..ef5dbb4 --- /dev/null +++ b/toota-palooza/cli.py @@ -0,0 +1,107 @@ +''' +Command line module for calling toota-palooza to do its work +''' + +__all__ = ['toota-palooza'] +__author__ = 'Paco Hope ' +__date__ = '25 November 2022' +__version__ = '1.0' +__copyright__ = 'Copyright © 2022 Paco Hope. See LICENSE for details.' + +from mastodon import Mastodon +from dotenv import load_dotenv +import os +import time +import argparse +import sys +from pprint import pprint +# import toota-palooza + + +def mastodonInit(): + """Connect to the Mastodon instance based on .env files""" + + server = Mastodon( + client_id = '.toota-palooza.env', + api_base_url = os.getenv('MD_HOST') + ) + + load_dotenv() + server.log_in( + os.getenv('MD_USER'), + os.getenv('MD_PASS'), + to_file = '.toota-palooza-usercred.env' + ) + return(server) + + +def checkPublicTimeline( server): + """Do one run. Connect to the database, connect to the server, get the public timeline. + Look at users and check to see if any are potential impersonators. Then exit.""" + + # Here's the idea: pick a chunkSize. Ask the server for that many toots off the public + # timeline. As long as the server gives us as many as we asked for, keep trying. + # as soon as we get less than we asked for, quit. + # + # XXX Not sure about rate-limiting + # + chunkSize = 20 + maxPosts = 1000 + timelineList = [] + useridList = {} + total = 0 + calls = 0 + while( total < maxPosts ): + timelineList = server.timeline(timeline='public', since_id=latestId, limit=chunkSize) + calls = calls + 1 + for post in timelineList: + userid = post.account.acct + username=userid.split('@')[0] + + try: + domain=userid.split('@')[1] + except IndexError: + # if there is no domain, then it's a local account + domain=os.getenv('MD_HOST').split('/')[2] + useridList[userid] = (username,domain,post.account.display_name,post.account.bot,post.url) + latestId=post.id + + if( len(timelineList) < 1): + # We got fewer than we asked for. Drop out of the loop. + total = total + len(timelineList) + break + else: + # record how many we did, and go again. + total = total + len(timelineList) + timelineList = [] + +def daemon_main(): + """Run from a command line.""" + server = mastodonInit() + + while(True): + # do a thing + time.sleep(600) + +def once(): + """Run from a command line.""" + server = mastodonInit() + return 0 + +def main(): + parser = argparse.ArgumentParser( + description='Check for suspicious impersonators.' + ) + parser.add_argument( '-d', '--debug', action='store_true', + help='Enable debugging messages.') + parser.add_argument( '-o', '--once', action='store_true', + help='Run once and exit. Default is to run as a daemon.') + args = parser.parse_args() + + if( args.once ): + exit(once()) + else: + daemon_main() + +if __name__ == '__main__': + exit(once()) \ No newline at end of file