diff --git a/README.md b/README.md index 44ed15b..2419b1b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,118 @@ # masto-gitsocial-bridge -bridge between mastodon and git social \ No newline at end of file +bridge between mastodon and bcrypt's [git-social](https://github.com/diracdeltas/tweets) + +## From git to masto + +Post on git-social... + +![Post on git social: This is what we call infrastructure shitposting](img/git-to-masto_0.png) + +Bridge to mastodon + +![Post on mastodon, same text but with link to the git commit](img/git-to-masto_1.png) + +## From masto to git + +Post on mastodon... + +![Post on mastodon: This is the future of decentralized communication](img/masto-to-git_0.png) + +Bridge to git-social + +![Post on git social: same post, but with link to tweet](img/masto-to-git_1.png) + +# Features + +Everything in this package is a bug and not a feature. + +# Setup + +## Installation + +I'm not going to uh, make this good or put it on pypi or anything. + +So you should clone this and install it with poetry, (otherwise +you have to modify the below `post-commit` action to activate the +venv where it is installed correctly) + +```shell +git clone https://git.jon-e.net/jonny/masto-gitsocial-bridge +cd masto-gitsocial-bridge +poetry install +``` + +## Make masto token + +~ check with your instance's policies before doing something bad like this ~ + +From your masto instance's homepage... + +- Preferences (in hamburger menu top right if page is narrow) +- Development tab +- New Application + +The bot needs permissions (I'm honestly not sure you need to give all of `read`, but +`Mastodon.py`'s `me()` method seems to need a lot of them. idk.) +- `read` +- `write:lists` - the bot only streams your posts by making a list with just you on it +- `write:statuses` - to xpost, dummy! + +Then copy the resulting access token for use in configuration... + +## Config + +See `masto_git_bridge.config.Config` for the required configuration values. + +The config object uses Pydantic to load either from an `.env` file or +from environment variables, for example, make an `.env` file in the cloned +repository directory like + +``` +MASTOGIT_MASTO_URL="https://social.coop" +MASTOGIT_MASTO_TOKEN="" +MASTOGIT_GIT_REPO="/path/to/your/git-social/tweets" +MASTOGIT_GIT_REMOTE_URL="https://git.jon-e.net/jonny/tweets" +MASTOGIT_LOGDIR="/wherever/you/want/to/put/logs" +``` + +This is assuming you don't need any sort of authentication/local password +on your local git repository in order to commit or push to it. + +## `post-commit` action + +In your git-social repository, make a `post-commit` action (`.git/hooks/post-commit`) +that looks something like this (see the [sample](post-commit.sample)) + +```bash +#!/bin/bash +# Assuming we have installed the package using poetry from a git repository +# lmao I did not say we handled virtual environments well in this package + +cd +poetry run post_last_commit + +# otherwise activate whatever venv you have installed the package in and +# call masto_git_bridge.main:post_last_commit, which is +# installed as an entrypoint script by poetry +``` + +# Usage + +The post-commit action should run anytime you commit a post to git-social, but to post +from mastodon you'll have to run the bot, which listens for your posts and reposts them to +git-social (only if they are "public" or "unlisted"). + +```bash +poetry run masto_gitbot +# or +# >>> poetry shell +# >>> masto_gitbot +# or however else you run python entrypoint scripts +# hell you could do python -m masto_git_bridge.main:masto_gitbot +# i think? +``` + +# Warnings & Gotchas + +If you ever enable this, you should promptly disable it \ No newline at end of file diff --git a/img/git-to-masto_0.png b/img/git-to-masto_0.png new file mode 100644 index 0000000..ced2120 Binary files /dev/null and b/img/git-to-masto_0.png differ diff --git a/img/git-to-masto_1.png b/img/git-to-masto_1.png new file mode 100644 index 0000000..da8c715 Binary files /dev/null and b/img/git-to-masto_1.png differ diff --git a/img/masto-to-git_0.png b/img/masto-to-git_0.png new file mode 100644 index 0000000..647311f Binary files /dev/null and b/img/masto-to-git_0.png differ diff --git a/img/masto-to-git_1.png b/img/masto-to-git_1.png new file mode 100644 index 0000000..7d84423 Binary files /dev/null and b/img/masto-to-git_1.png differ diff --git a/masto_git_bridge/bot.py b/masto_git_bridge/bot.py index 923e5bd..c13b03b 100644 --- a/masto_git_bridge/bot.py +++ b/masto_git_bridge/bot.py @@ -1,40 +1,106 @@ +import pdb from typing import Optional from datetime import datetime from masto_git_bridge.config import Config -from masto_git_bridge.models import Account +from masto_git_bridge.models import Account, List +from masto_git_bridge.post import Post, Status +from masto_git_bridge.logger import init_logger +from masto_git_bridge.repo import Repo from mastodon import Mastodon, StreamListener class Listener(StreamListener): def __init__(self, client: Mastodon, config:Optional[Config]=None): + super(Listener, self).__init__() self.client = client if config is None: config = Config() - self.config = config + self.logger = init_logger('mastogit_bot-stream', basedir=self.config.LOGDIR) + + self.repo = Repo(path=config.GIT_REPO) + + + + def on_update(self, status:dict): + status = Status(**status) + if status.visibility in ('private', 'direct'): + # not xposting dms + self.logger.info('Not xposting private messages') + return + + post = Post.from_status(status) + if post.text.startswith('xpost'): + self.logger.info('Not xposting an xpost') + return + + success = self.repo.post(post.format_commit()) + if success: + self.logger.info('Posted to git!') + else: + self.logger.exception('Failed to post to git!') + + + class Bot: - def __init__(self, config:Optional[Config]=None): + def __init__(self, config:Optional[Config]=None, post_length=500): self._me = None # type: Optional[Account] + self._me_list = None # type: Optional[List] if config is None: config = Config() self.config = config self.config.LOGDIR.mkdir(exist_ok=True) + self.post_length = post_length + self.logger = init_logger('mastogit_bot', basedir=self.config.LOGDIR) self.client = Mastodon( access_token=self.config.MASTO_TOKEN, api_base_url=self.config.MASTO_URL ) + def init_stream(self, run_async:bool=True): + # Listen to a stream consisting of just us. + listener = Listener(client=self.client, config=self.config) + self.logger.info('Initializing streaming') + self.client.stream_list( + self.me_list.id, + listener = listener, + run_async=run_async + ) + + def post(self, post:str): + # TODO: Split long posts + if len(post)>self.post_length: + raise NotImplementedError(f"Cant split long posts yet, got post of length {len(post)} when max length is {self.post_length}") + + self.client.status_post(post) + self.logger.info(f"Posted:\n{post}") + @property def me(self) -> Account: if self._me is None: self._me = Account(**self.client.me()) return self._me + def _make_me_list(self) -> List: + me_list = List(**self.client.list_create('me')) + self.client.list_accounts_add(me_list.id, [self.me.id]) + self.logger.info('Created list with just me in it!') + return me_list + @property + def me_list(self) -> List: + if self._me_list is None: + lists = self.client.lists() + me_list = [l for l in lists if l.get('title', '') == 'me'] + if len(me_list)>0: + self._me_list = List(**me_list[0]) + else: + self._me_list = self._make_me_list() + return self._me_list diff --git a/masto_git_bridge/logger.py b/masto_git_bridge/logger.py new file mode 100644 index 0000000..1601aed --- /dev/null +++ b/masto_git_bridge/logger.py @@ -0,0 +1,56 @@ +import logging +from rich.logging import RichHandler +from pathlib import Path +import sys +import typing +from typing import Optional, Union, Tuple, List, Dict, Literal +from logging.handlers import RotatingFileHandler + +def init_logger( + name:Optional[str]=None, + basedir:Optional[Path]=None, + loglevel:str='DEBUG', + loglevel_disk:Optional[str]='DEBUG' + ): + if name is None: + name = 'wiki_postbot' + else: + if not name.startswith('wiki_postbot'): + name = '.'.join(['wiki_postbot', name]) + + if loglevel_disk is None: + loglevel_disk = loglevel + + logger = logging.getLogger(name) + logger.setLevel(loglevel) + + + if basedir is not None: + logger.addHandler(_file_handler(basedir, name, loglevel_disk)) + + logger.addHandler(_rich_handler()) + return logger + + +def _file_handler(basedir:Path, name:str, loglevel:str="DEBUG") -> RotatingFileHandler: + filename = Path(basedir) / '.'.join([name, 'log']) + basedir.mkdir(parents=True, exist_ok=True) + file_handler = RotatingFileHandler( + str(filename), + mode='a', + maxBytes=2 ** 24, + backupCount=5 + ) + file_formatter = logging.Formatter("[%(asctime)s] %(levelname)s [%(name)s]: %(message)s") + file_handler.setLevel(loglevel) + file_handler.setFormatter(file_formatter) + return file_handler + +def _rich_handler() -> RichHandler: + rich_handler = RichHandler(rich_tracebacks=True, markup=True) + rich_formatter = logging.Formatter( + "[bold green]\[%(name)s][/bold green] %(message)s", + datefmt='[%y-%m-%dT%H:%M:%S]' + ) + rich_handler.setFormatter(rich_formatter) + return rich_handler \ No newline at end of file diff --git a/masto_git_bridge/main.py b/masto_git_bridge/main.py index d06d5b8..aace5d2 100644 --- a/masto_git_bridge/main.py +++ b/masto_git_bridge/main.py @@ -1,10 +1,42 @@ from typing import Optional from masto_git_bridge.config import Config from masto_git_bridge.repo import Repo +from masto_git_bridge.bot import Bot +from masto_git_bridge.post import Post +from masto_git_bridge.logger import init_logger +from time import sleep def post_last_commit(config:Optional[Config]=None): + """ + Should be triggered as a commit hook because it doesn't validate + the last commit hasn't already been posted. + """ if config is None: config = Config() + logger = init_logger('post-git', basedir=config.LOGDIR) repo = Repo(config.GIT_REPO) last_commit = repo.last_commit + post = Post.from_commit(last_commit) + + if post.text.startswith('xpost'): + logger.info('Not xposting an xpost') + return + + + bot = Bot(config=config) + bot.post(post.format_masto()) + +def masto_gitbot(config:Optional[Config]=None): + if config is None: + config = Config() + + bot = Bot(config=config) + try: + bot.init_stream() + while True: + sleep(60*60) + bot.logger.info('taking a breath') + except KeyboardInterrupt: + bot.logger.info('quitting!') + diff --git a/masto_git_bridge/models.py b/masto_git_bridge/models.py index 6429adc..3b15172 100644 --- a/masto_git_bridge/models.py +++ b/masto_git_bridge/models.py @@ -1,5 +1,13 @@ from pydantic import BaseModel +class List(BaseModel): + """A mastodon list!""" + id: str + title: str + + class Config: + extra = 'ignore' + class Account(BaseModel): """Not transcribing full model now, just using to check""" acct: str diff --git a/masto_git_bridge/post.py b/masto_git_bridge/post.py index 1bbe827..c83a931 100644 --- a/masto_git_bridge/post.py +++ b/masto_git_bridge/post.py @@ -1,8 +1,68 @@ -from typing import Optional +from typing import Optional, Literal from datetime import datetime from pydantic import BaseModel +import re +from bs4 import BeautifulSoup +from masto_git_bridge.repo import Commit +from masto_git_bridge.models import Account + +class Status(BaseModel): + """ + Model of a toot on mastodon + + See: https://mastodonpy.readthedocs.io/en/stable/#toot-dicts + """ + id: int + url: str + account: Account + content: str + visibility: Literal['public', 'unlisted', 'private', 'direct'] + in_reply_to_id: Optional[int] = None + in_reply_to_account_id: Optional[int] = None + + class Config: + extra='ignore' class Post(BaseModel): - timestamp: datetime - hash: Optional[str] + #timestamp: Optional[datetime] = None + text:str + status:Optional[Status] = None + commit:Optional[Commit] = None + + @classmethod + def from_commit(cls, commit:Commit) -> 'Post': + text = '\n'.join([commit.subject, commit.body]) + return Post(text=text, commit=commit) + + @classmethod + def from_status(cls, status:Status) -> 'Post': + # split paragraphs using bs4 + soup = BeautifulSoup(status.content, 'lxml') + # replace with double line breaks + pars = [p.text for p in soup.find_all('p')] + text = '\n\n'.join(pars) + return Post(text=text, status=status) + + def format_masto(self) -> str: + """ + Format a post to go from git -> masto. + + Needs to have a :attr:`.commit` attribute! + + Does not split the body text into multiple toots. + That should be handled in the posting action + + Example: + + git-social: https://{repo_url}/commits/{hash} + {subject line} + {body} + """ + return f"xpost from git-social: {self.commit.url}\n---\n{self.text}""" + + def format_commit(self) -> str: + """ + Add a link back to original masto post split by double lines + """ + return f"xpost from mastodon: {self.status.url}\n\n{self.text}" \ No newline at end of file diff --git a/masto_git_bridge/repo.py b/masto_git_bridge/repo.py index 269f3f2..4ec1e0b 100644 --- a/masto_git_bridge/repo.py +++ b/masto_git_bridge/repo.py @@ -1,9 +1,10 @@ +import pdb from pathlib import Path from subprocess import run import json from urllib.parse import urljoin -from pydantic import BaseModel, EmailStr +from pydantic import BaseModel, EmailStr, AnyHttpUrl log_format = '{%n "commit": "%H",%n "abbreviated_commit": "%h",%n "tree": "%T",%n "abbreviated_tree": "%t",%n "parent": "%P",%n "abbreviated_parent": "%p",%n "refs": "%D",%n "encoding": "%e",%n "subject": "%s",%n "sanitized_subject_line": "%f",%n "body": "%b",%n "commit_notes": "%N",%n "verification_flag": "%G?",%n "signer": "%GS",%n "signer_key": "%GK",%n "author": {%n "name": "%aN",%n "email": "%aE",%n "date": "%aD"%n },%n "commiter": {%n "name": "%cN",%n "email": "%cE",%n "date": "%cD"%n }%n}' """Thanks https://gist.github.com/varemenos/e95c2e098e657c7688fd""" @@ -20,9 +21,11 @@ class Commit(BaseModel): body:str author: Author commiter: Author + origin_url: AnyHttpUrl - def make_url(self, remote_url:str): - return urljoin(remote_url, f'commit/{self.abbreviated_commit}') + @property + def url(self) -> str: + return urljoin(self.origin_url + '/', f'commit/{self.abbreviated_commit}') class Config: extra='ignore' @@ -31,6 +34,33 @@ class Repo: def __init__(self, path:Path): self.path = Path(path) + def post(self, post:str) -> bool: + # make paragraphs by splitting \n\n + paras = [] + for para in post.split('\n\n'): + paras.extend(('-m', para)) + + path = self.path + command = [ + 'git', + '-C', str(path), + 'commit', + *paras, + '--allow-empty' + ] + output = run(command, capture_output=True) + if output.returncode != 0: + return False + + output = run([ + 'git', + '-C', str(path), + 'push' + ], capture_output=True) + return True + + + @property def last_commit(self) -> Commit: path = self.path @@ -44,4 +74,17 @@ class Repo: capture_output=True ) parse = json.loads(output.stdout, strict=False) - return Commit(**parse) \ No newline at end of file + return Commit(origin_url=self.origin_url, **parse) + + @property + def origin_url(self) -> str: + path = self.path + output = run([ + "git", + '-C', str(path), + 'remote', + 'get-url', + 'origin' + ], capture_output=True) + url = output.stdout.decode('utf-8').strip().rstrip('.git') + return url diff --git a/poetry.lock b/poetry.lock index 13fc3c1..ab8093f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,18 @@ +[[package]] +name = "beautifulsoup4" +version = "4.11.1" +description = "Screen-scraping library" +category = "main" +optional = false +python-versions = ">=3.6.0" + +[package.dependencies] +soupsieve = ">1.2" + +[package.extras] +html5lib = ["html5lib"] +lxml = ["lxml"] + [[package]] name = "blurhash" version = "1.1.4" @@ -28,6 +43,17 @@ python-versions = ">=3.6.0" [package.extras] unicode-backport = ["unicodedata2"] +[[package]] +name = "commonmark" +version = "0.9.1" +description = "Python parser for the CommonMark Markdown spec" +category = "main" +optional = false +python-versions = "*" + +[package.extras] +test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"] + [[package]] name = "decorator" version = "5.1.1" @@ -72,6 +98,20 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "lxml" +version = "4.9.1" +description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" + +[package.extras] +cssselect = ["cssselect (>=0.7)"] +html5 = ["html5lib"] +htmlsoup = ["BeautifulSoup4"] +source = ["Cython (>=0.29.7)"] + [[package]] name = "mastodon-py" version = "1.5.2" @@ -111,6 +151,17 @@ typing-extensions = ">=4.1.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pygments" +version = "2.13.0" +description = "Pygments is a syntax highlighting package written in Python." +category = "main" +optional = false +python-versions = ">=3.6" + +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "python-dateutil" version = "2.8.2" @@ -167,6 +218,21 @@ urllib3 = ">=1.21.1,<1.27" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "rich" +version = "12.6.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +category = "main" +optional = false +python-versions = ">=3.6.3,<4.0.0" + +[package.dependencies] +commonmark = ">=0.9.0,<0.10.0" +pygments = ">=2.6.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] + [[package]] name = "six" version = "1.16.0" @@ -175,6 +241,14 @@ category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +[[package]] +name = "soupsieve" +version = "2.3.2.post1" +description = "A modern CSS selector implementation for Beautiful Soup." +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "typing-extensions" version = "4.4.0" @@ -199,9 +273,13 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "61c87bda2ae45e82c6ed4dca57ad766a01078f32838844c10832c9e5f5790ab3" +content-hash = "c85c922dc9bcac039fd7b4b5f12d310934ae4faa2bd23c426c2a3beb62e86721" [metadata.files] +beautifulsoup4 = [ + {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, + {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, +] blurhash = [ {file = "blurhash-1.1.4-py2.py3-none-any.whl", hash = "sha256:7611c1bc41383d2349b6129208587b5d61e8792ce953893cb49c38beeb400d1d"}, {file = "blurhash-1.1.4.tar.gz", hash = "sha256:da56b163e5a816e4ad07172f5639287698e09d7f3dc38d18d9726d9c1dbc4cee"}, @@ -214,6 +292,10 @@ charset-normalizer = [ {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] +commonmark = [ + {file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"}, + {file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"}, +] decorator = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, @@ -230,6 +312,78 @@ idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] +lxml = [ + {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, + {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"}, + {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"}, + {file = "lxml-4.9.1-cp27-cp27m-win32.whl", hash = "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3"}, + {file = "lxml-4.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627"}, + {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84"}, + {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5"}, + {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8"}, + {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8"}, + {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d"}, + {file = "lxml-4.9.1-cp310-cp310-win32.whl", hash = "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7"}, + {file = "lxml-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b"}, + {file = "lxml-4.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d"}, + {file = "lxml-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3"}, + {file = "lxml-4.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29"}, + {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d"}, + {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318"}, + {file = "lxml-4.9.1-cp35-cp35m-win32.whl", hash = "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7"}, + {file = "lxml-4.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4"}, + {file = "lxml-4.9.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b"}, + {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf"}, + {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3"}, + {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391"}, + {file = "lxml-4.9.1-cp36-cp36m-win32.whl", hash = "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e"}, + {file = "lxml-4.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7"}, + {file = "lxml-4.9.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3"}, + {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca"}, + {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785"}, + {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785"}, + {file = "lxml-4.9.1-cp37-cp37m-win32.whl", hash = "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a"}, + {file = "lxml-4.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e"}, + {file = "lxml-4.9.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130"}, + {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715"}, + {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036"}, + {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387"}, + {file = "lxml-4.9.1-cp38-cp38-win32.whl", hash = "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94"}, + {file = "lxml-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345"}, + {file = "lxml-4.9.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91"}, + {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000"}, + {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25"}, + {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd"}, + {file = "lxml-4.9.1-cp39-cp39-win32.whl", hash = "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb"}, + {file = "lxml-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b"}, + {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2"}, + {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73"}, + {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c"}, + {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"}, + {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, +] mastodon-py = [ {file = "Mastodon.py-1.5.2-py2.py3-none-any.whl", hash = "sha256:49afbf9f4347f355bee5638b71a5231b0e1164a58d253ccd9b4345d999c43369"}, {file = "Mastodon.py-1.5.2.tar.gz", hash = "sha256:c98fd97b7450cd02262669b80be20f53657b5540c4888a47231df11856910918"}, @@ -272,6 +426,10 @@ pydantic = [ {file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"}, {file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"}, ] +pygments = [ + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, +] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, @@ -292,10 +450,18 @@ requests = [ {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, ] +rich = [ + {file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"}, + {file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] +soupsieve = [ + {file = "soupsieve-2.3.2.post1-py3-none-any.whl", hash = "sha256:3b2503d3c7084a42b1ebd08116e5f81aadfaea95863628c80a3b774a11b7c759"}, + {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, +] typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, diff --git a/post-commit.sample b/post-commit.sample new file mode 100644 index 0000000..aa500e4 --- /dev/null +++ b/post-commit.sample @@ -0,0 +1,10 @@ +#!/bin/bash +# Assuming we have installed the package using poetry from a git repository +# lmao I did not say we handled virtual environments well in this package + +cd +poetry run post_last_commit + +# otherwise activate whatever venv you have installed the package in and +# call masto_git_bridge.main:post_last_commit, which is +# installed as an entrypoint script by poetry \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8b42c75..b7b1353 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,13 @@ packages = [{include = "masto_git_bridge"}] python = "^3.9" "Mastodon.py" = "^1.5.2" pydantic = {extras = ["dotenv", "email"], version = "^1.10.2"} +rich = "^12.6.0" +beautifulsoup4 = "^4.11.1" +lxml = "^4.9.1" +[tool.poetry.scripts] +post_last_commit = 'masto_git_bridge.main:post_last_commit' +masto_gitbot = 'masto_git_bridge.main:masto_gitbot' [build-system] requires = ["poetry-core"]