diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f9acb49..21b3ded 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -14,8 +14,7 @@ "forwardPorts": [8000], "portsAttributes": { "8000": { - "label": "Sphinx", - "onAutoForward": "openPreview" + "label": "Sphinx" } }, diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4a07f4..08a848d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: paths: - '**.py' + - '**.rst' - '.github/workflows/ci.yml' jobs: @@ -26,17 +27,21 @@ jobs: - name: Install dependencies run: | - python -m pip install --upgrade pip pylint black mypy + python -m pip install --upgrade pip pylint black mypy sphinx-lint pip install -r requirements.txt - name: Run Pylint run: | - pylint $(git ls-files '*.py') + pylint $(git ls-files 'src/**/*.py') - name: Run Black run: | - black $(git ls-files '*.py') --check + black $(git ls-files 'src/**/*.py') --check - name: Run Mypy run: | - mypy $(git ls-files '*.py') + mypy $(git ls-files 'src/**/*.py') + + - name: Run Sphinx Lint + run: | + sphinx-lint $(git ls-files '*.rst') \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2773ad3..1bf4ac5 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -27,6 +27,12 @@ "-e" ], "problemMatcher": [] + }, + { + "label": "Build documentation", + "type": "shell", + "command": "sphinx-autobuild docs/source docs/_build/html", + "problemMatcher": [] } ] } diff --git a/docs/source/_static/.gitkeep b/docs/source/_static/.gitkeep deleted file mode 100644 index 8b13789..0000000 --- a/docs/source/_static/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/source/_static/integrations-tab.png b/docs/source/_static/integrations-tab.png new file mode 100644 index 0000000..298e136 Binary files /dev/null and b/docs/source/_static/integrations-tab.png differ diff --git a/docs/source/_static/webhook-message.png b/docs/source/_static/webhook-message.png new file mode 100644 index 0000000..d22fc88 Binary files /dev/null and b/docs/source/_static/webhook-message.png differ diff --git a/docs/source/_static/webhook-settings.png b/docs/source/_static/webhook-settings.png new file mode 100644 index 0000000..83475f5 Binary files /dev/null and b/docs/source/_static/webhook-settings.png differ diff --git a/docs/source/conf.py b/docs/source/conf.py index 207f6d1..ccb8040 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -3,20 +3,32 @@ # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('../..')) + + # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information project = 'InstaWebhooks' copyright = '2024, Ryan Luu' author = 'Ryan Luu' -release = '0.1' -version = '0.1.3' +release = '1.0' +version = '1.0.0' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx_copybutton' + 'sphinx_copybutton', + 'sphinxarg.ext', ] templates_path = ['_templates'] @@ -31,6 +43,28 @@ html_static_path = ['_static'] html_theme_options = { + "footer_icons": [ + { + "name": "GitHub", + "url": "https://github.com/RyanLua/InstaWebhooks", + "html": """ + + + + """, + "class": "", + }, + { + "name": "Read the Docs", + "url": "https://readthedocs.org/projects/instawebhooks", + "html": """ + + + + """, + "class": "", + }, + ], "announcement": "This is a early version of the documentation and not final.", "source_repository": "https://github.com/RyanLua/InstaWebhooks", "source_branch": "main", diff --git a/docs/source/getting-started.rst b/docs/source/getting-started.rst new file mode 100644 index 0000000..9434871 --- /dev/null +++ b/docs/source/getting-started.rst @@ -0,0 +1,78 @@ +Getting Started +=============== + +Learn about getting started with InstaWebhooks to send new Instagram posts any Discord channel from scratch. + +Installing InstaWebhooks +------------------------ + +With `Python `_ installed, install InstaWebhooks with `pip `_: + +.. code:: console + + $ pip install instawebhooks + +Check that InstaWebhooks was installed correctly by seeing if it reports its version: + +.. code:: console + + $ instawebhooks --version + +Make sure that are on the `latest version of InstaWebhooks `_. + +For more ways to install InstaWebhooks, see the `installation guide `_. + +Setting up Discord webhooks +--------------------------- + +To get your Discord webhook URL, you need the **Manage Webhooks** permission in the channel you want to send new Instagram posts to. + +You can learn more about webhooks through the article, `Intro to Webhooks `_. + +Creating your webhook +^^^^^^^^^^^^^^^^^^^^^ + +If your already have a webhook, you can skip this step. + +#. Open **Server Settings**, then **Integrations** +#. Click the "**Create Webhook**" button + +.. image:: _static/integrations-tab.png + +Now you can set the name, channel, and avatar for the webhook. + +Getting the webhook URL +^^^^^^^^^^^^^^^^^^^^^^^ + +When you have your webhook made, click the "**Copy Webhook URL**" button to copy the URL to your clipboard. + +.. image:: _static/webhook-settings.png + +The copied URL should look similar this: + +.. code:: none + + https://discordapp.com/api/webhooks/0123456789/abcdefghijklmnopqrstuvwxyz + +Setting up InstaWebhooks +------------------------ + +Now with the webhook URL and a Instagram account in mind, you can set up InstaWebhooks to send new Instagram posts to your Discord channel. + +Replace ```` with the username of the Instagram account you want to monitor and ```` with the Discord webhook URL you copied earlier. + +.. code:: console + + $ instawebhooks + +It should look something like this: + +.. code:: console + + $ instawebhooks raenlua https://discord.com/api/webhooks/0123456789/abcdefghijklmnopqrstuvwxyz + +Now, whenever the Instagram account `@raenlua` posts a new photo, it will be sent to the Discord webhook. + +.. image:: _static/webhook-message.png + +For more information about using InstaWebhooks, see the `usage guide `_. diff --git a/docs/source/index.rst b/docs/source/index.rst index 08d0250..a196444 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,6 +8,10 @@ InstaWebhooks documentation InstaWebhooks is a `Python `_ command-line interface which allows you to monitor any Instagram account for new posts and then send them to a `Discord webhook `_. +* Works with **any Instagram account**, including private accounts if you are a follower +* Customizable **Discord embeds** for new posts and message contents including **mentions/pings** +* **User-definable refresh interval** for checking for new posts the second they are posted + Quickstart ---------- @@ -30,13 +34,14 @@ Contents .. toctree:: + getting-started installation usage .. toctree:: :caption: Project - Contributing - Code of Conduct + Contributing + Code of Conduct GitHub - PyPI \ No newline at end of file + PyPI diff --git a/docs/source/installation.rst b/docs/source/installation.rst index c022cbc..d6e0c30 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -90,4 +90,4 @@ Installing from source code is another option to contribute or use the latest de .. code:: console - $ pip install --editable . \ No newline at end of file + $ pip install --editable . diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 9470763..73d7370 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -6,75 +6,35 @@ To see the available options and arguments, run the following command: .. code:: console $ instawebhooks --help - usage: instawebhooks [-h] [-q | -v] [-i REFRESH_INTERVAL] [-c MESSAGE_CONTENT] [-e] [--version] - instagram_username discord_webhook_url - - Monitor Instagram accounts for new posts and send them to a Discord webhook - - positional arguments: - instagram_username the Instagram username to monitor for new posts - discord_webhook_url the Discord webhook URL to send new posts to - - options: - -h, --help show this help message and exit - -q, --quiet hide all logging - -v, --verbose show debug logging - -i REFRESH_INTERVAL, --refresh-interval REFRESH_INTERVAL - time in seconds to wait before checking for new posts again - -c MESSAGE_CONTENT, --message-content MESSAGE_CONTENT - the message content to send with the webhook - -e, --no-embed don't show the post embed and only send message content - --version show program's version number and exit - - https://github.com/RaenLua/InstaWebhooks - -Below, learn how to use InstaWebhooks and what you can do with it. Examples -------- -In the below templates, replace ```` with the Instagram username you want to monitor and ```` with the Discord webhook URL you want to send new posts to. - -Your command should look similar to this: - -.. code:: console - - $ instawebhooks raenlua https://discord.com/api/webhooks/0123456789/abcdefghijklmnopqrstuvwxyz - -Templates ---------- - -Example templates for using InstaWebhooks are provided below. Note to change the Instagram username and Discord webhook URL to your own. - -.. note:: - - The default refresh interval is 1 hour (3600 seconds), and the default message content is an empty string. - -Send new posts every hour: +* Send new posts: .. code:: console $ instawebhooks -Send new posts every hour with verbose logging: +* Send new posts with verbose logging: .. code:: console $ instawebhooks -v -Send new posts every 30 minutes: +* Send new posts every 30 minutes: .. code:: console $ instawebhooks -i 1800 -Send new posts every hour with a custom message: +* Send new posts with a custom message: .. code:: console $ instawebhooks -c "New post from {owner_name}: {post_url}" -Send new posts every hour with no embed and a custom message: +* Send new posts with no embed and a custom message: .. code:: console @@ -83,77 +43,22 @@ Send new posts every hour with no embed and a custom message: Reference --------- -Positional Arguments -~~~~~~~~~~~~~~~~~~~~ - -``INSTAGRAM_USERNAME`` - - The Instagram username to monitor for new posts. - - Usernames must follow the Instagram username format: - - * Starts with a letter or underscore. - * Does not contain consecutive periods. - * Is between 2 and 30 characters long. - * Ends with an alphanumeric character or underscore. - -``DISCORD_WEBHOOK_URL`` - - The Discord webhook URL to send new posts to. - - URLs must follow the Discord webhook URL format: - - * ``https://discord.com/api/webhooks/{webhook_id}/{webhook_token}`` - * ``https://discordapp.com/api/webhooks/{webhook_id}/{webhook_token}`` - -Optional Arguments -~~~~~~~~~~~~~~~~~~ - -``-h, --help`` - - Show this help message and exit. - -``-v, --verbose`` - - Enable verbose logging. - - Changes the logging level to debug, showing the following logs in addition to the default info logs: - - * When a check for new posts is started. - * If a new post is found or not. - * When a post is sent to Discord. - -``-i REFRESH_INTERVAL, --refresh-interval REFRESH_INTERVAL`` - - .. caution:: - - Do not set the refresh interval too low or you may be `rate limited by Instagram `_. - - The refresh interval to check for new posts in seconds (default: 3600). - -``-c MESSAGE_CONTENT, --message-content MESSAGE_CONTENT`` - - The message content to send to Discord (default: ""). - - Accepts placeholders for the post information: +.. argparse:: + :module: src.instawebhooks.parser + :func: parser + :prog: instawebhooks + :noepilog: - * ``{post_url}`` - The URL to the post on Instagram - * ``https://www.instagram.com/C8wRGmyR-6N`` - * ``{owner_url}`` - The URL to the owner's profile on Instagram - * ``https://www.instagram.com/raenlua`` - * ``{owner_name}`` - The owner's full name - * ``Ryan Luu`` - * ``{owner_username}`` - The owner's username - * ``raenlua`` - * ``{post_caption}`` - The post's caption - * ``This is a post caption.`` - * ``{post_shortcode}`` - The post's shortcode - * ``C8wRGmyR-6N`` - * ``{post_image_url}`` - The post's image URL - * ``https://www.instagram.com/p/C8wRGmyR-6N/media`` + instagram_username : @after + Usernames must follow the Instagram username format: -``-e, --no-embed`` + * Starts with a letter or underscore. + * Does not contain consecutive periods. + * Is between 2 and 30 characters long. + * Ends with an alphanumeric character or underscore. - Don't show the post embed and only send message content + discord_webhook_url : @after + URLs must follow the Discord webhook URL format: - A message content must be provided when using this option. Empty messages cannot be sent. \ No newline at end of file + * ``https://discord.com/api/webhooks/{webhook_id}/{webhook_token}`` + * ``https://discordapp.com/api/webhooks/{webhook_id}/{webhook_token}`` diff --git a/requirements.txt b/requirements.txt index e52a6b2..e3d9716 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/src/instawebhooks/__main__.py b/src/instawebhooks/__main__.py index f424ec2..baaf657 100644 --- a/src/instawebhooks/__main__.py +++ b/src/instawebhooks/__main__.py @@ -1,16 +1,16 @@ """Module for sending new Instagram posts to Discord.""" import asyncio -import importlib.metadata import io import logging import re import sys -from argparse import ArgumentParser -from typing import Dict from datetime import datetime, timedelta from itertools import dropwhile, takewhile from time import sleep +from typing import Dict + +from .parser import parser try: from aiohttp import ClientSession @@ -24,19 +24,6 @@ ) from exc -def regex(pattern: str): - """Argument type for matching a regex pattern""" - - def closure_check_regex(arg_value: str): - if not re.match(pattern, arg_value): - raise ValueError(f"invalid value: '{arg_value}'") - return arg_value - - return closure_check_regex - - -version = importlib.metadata.version("instawebhooks") - # Set up logging logger = logging.getLogger(__name__) logging.basicConfig( @@ -45,50 +32,6 @@ def closure_check_regex(arg_value: str): level=logging.INFO, ) -# Parse command line arguments -parser = ArgumentParser( - prog="instawebhooks", - description=( - "Monitor Instagram accounts for new posts and send them to a Discord webhook" - ), - epilog="https://github.com/RaenLua/InstaWebhooks", -) -group = parser.add_mutually_exclusive_group() -parser.add_argument( - "instagram_username", - help="the Instagram username to monitor for new posts", - type=regex(r"^[a-zA-Z_](?!.*?\.{2})[\w.]{1,28}[\w]$"), -) -parser.add_argument( - "discord_webhook_url", - help="the Discord webhook URL to send new posts to", - type=regex( - r"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-zA-Z0-9_.-]*)$" - ), -) -group.add_argument("-q", "--quiet", help="hide all logging", action="store_true") -group.add_argument("-v", "--verbose", help="show debug logging", action="store_true") -parser.add_argument( - "-i", - "--refresh-interval", - help="time in seconds to wait before checking for new posts again", - type=int, - default=3600, -) -parser.add_argument( - "-c", - "--message-content", - help="the message content to send with the webhook", - type=str, - default="", -) -parser.add_argument( - "-e", - "--no-embed", - help="don't show the post embed and only send message content", - action="store_true", -) -parser.add_argument("--version", action="version", version="%(prog)s " + version) args = parser.parse_args() # Set the logger to debug if verbose is enabled diff --git a/src/instawebhooks/parser.py b/src/instawebhooks/parser.py new file mode 100644 index 0000000..8753c1a --- /dev/null +++ b/src/instawebhooks/parser.py @@ -0,0 +1,67 @@ +"""Command line argument parser for InstaWebhooks""" + +import importlib.metadata +import re +from argparse import ArgumentParser + + +def regex(pattern: str): + """Argument type for matching a regex pattern""" + + def closure_check_regex(arg_value: str): + if not re.match(pattern, arg_value): + raise ValueError(f"invalid value: '{arg_value}'") + return arg_value + + return closure_check_regex + + +try: + VERSION = importlib.metadata.version("instawebhooks") +except importlib.metadata.PackageNotFoundError: + VERSION = "unknown" + +# Parse command line arguments +parser = ArgumentParser( + prog="instawebhooks", + description=( + "Monitor Instagram accounts for new posts and send them to a Discord webhook" + ), + epilog="https://github.com/RaenLua/InstaWebhooks", +) +group = parser.add_mutually_exclusive_group() +parser.add_argument( + "instagram_username", + help="the Instagram username to monitor for new posts", + type=regex(r"^[a-zA-Z_](?!.*?\.{2})[\w.]{1,28}[\w]$"), +) +parser.add_argument( + "discord_webhook_url", + help="the Discord webhook URL to send new posts to", + type=regex( + r"^.*(discord|discordapp)\.com\/api\/webhooks\/([\d]+)\/([a-zA-Z0-9_.-]*)$" + ), +) +group.add_argument("-q", "--quiet", help="hide all logging", action="store_true") +group.add_argument("-v", "--verbose", help="show debug logging", action="store_true") +parser.add_argument( + "-i", + "--refresh-interval", + help="time in seconds to wait before checking for new posts again", + type=int, + default=3600, +) +parser.add_argument( + "-c", + "--message-content", + help="the message content to send with the webhook", + type=str, + default="", +) +parser.add_argument( + "-e", + "--no-embed", + help="don't show the post embed and only send message content", + action="store_true", +) +parser.add_argument("--version", action="version", version="%(prog)s " + VERSION)