Compare commits
2 commits
6c65eaa301
...
843f384083
Author | SHA1 | Date | |
---|---|---|---|
843f384083 | |||
671619526e |
16 changed files with 823 additions and 435 deletions
68
README.md
68
README.md
|
@ -1,10 +1,40 @@
|
|||
# masto-gitsocial-bridge
|
||||
|
||||
bridge between mastodon and bcrypt's [git-social](https://github.com/diracdeltas/tweets)
|
||||
# masto-bridges (formerly masto-gitsocial-bridge)
|
||||
|
||||
Mirrored to https://github.com/sneakers-the-rat/masto-gitsocial-bridge if for some godforsaken reason you want to raise issues or pull requests or fork it or whatever
|
||||
|
||||
## From git to masto
|
||||
|
||||
## CalDAV to Masto (and back again)
|
||||
|
||||
Post directly from your (CalDAV) calendar, and follow your feeds from your calendar!
|
||||
|
||||
See [config](#Config) below for how to set up for your calendar.
|
||||
|
||||
Compatible with mobile too!
|
||||
|
||||
![Post from your calendar, get your feed from your calendar](img/masto-caldav.png)
|
||||
|
||||
### From Calendar to Masto
|
||||
|
||||
Make a new calendar event with
|
||||
- Event title/summary: your username `user@instance.com` or just `user`. Calendar entries that don't
|
||||
match your username will not be posted (since they aren't from you!)
|
||||
- Notes/Description: The content of your post!
|
||||
- Time, date, duration, repetition: don't matter for now!
|
||||
- The absence of a URL in the "location" field is used to make sure we don't echo posts we make from the real masto
|
||||
|
||||
### From Masto to Calendar
|
||||
|
||||
When running `masto_bridgebot` with `MASTOBRIDGE_ENABLE_CALDAV=true` and
|
||||
`MASTOBRIDGE_STREAM_MODE="home"`, your home feed should already be posting to your calendar!
|
||||
|
||||
To make it so only your posts are added to your calendar, set the stream mode to `'list'`
|
||||
|
||||
## git-social
|
||||
|
||||
bridge between mastodon and bcrypt's [git-social](https://github.com/diracdeltas/tweets)
|
||||
|
||||
|
||||
### From git to masto
|
||||
|
||||
Post on git-social...
|
||||
|
||||
|
@ -14,7 +44,7 @@ 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
|
||||
### From masto to git
|
||||
|
||||
Post on mastodon...
|
||||
|
||||
|
@ -71,11 +101,25 @@ from environment variables, for example, make an `.env` file in the cloned
|
|||
repository directory like
|
||||
|
||||
```
|
||||
MASTOGIT_MASTO_URL="https://social.coop"
|
||||
MASTOGIT_MASTO_TOKEN="<mastodon bot access 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"
|
||||
MASTOBRIDGE_LOGDIR="/wherever/you/want/to/put/logs"
|
||||
|
||||
MASTOBRIDGE_MASTO_URL="https://social.coop"
|
||||
MASTOBRIDGE_MASTO_TOKEN="<mastodon bot access token>"
|
||||
|
||||
MASTOBRIDGE_ENABLE_GIT=false
|
||||
MASTOBRIDGE_GIT_REPO="/path/to/your/git-social/tweets"
|
||||
MASTOBRIDGE_GIT_REMOTE_URL="https://git.jon-e.net/jonny/tweets"
|
||||
|
||||
MASTOBRIDGE_ENABLE_CALDAV=true
|
||||
MASTOBRIDGE_CALDAV_URL="https://your.caldav.calendar/wherever"
|
||||
MASTOBRIDGE_CALDAV_USER="<caldav username>"
|
||||
MASTOBRIDGE_CALDAV_PASSWORD="<caldav password>"
|
||||
MASTOBRIDGE_CALDAV_CALENDAR_NAME="<name of calendar to use for feeds and posts>"
|
||||
|
||||
# "home" (for streaming your home feed to bridges)
|
||||
# or "list" for streaming a list with just your account in it to bridges
|
||||
MASTOBRIDGE_STREAM_MODE="home"
|
||||
|
||||
```
|
||||
|
||||
This is assuming you don't need any sort of authentication/local password
|
||||
|
@ -109,9 +153,9 @@ git-social (only if they are "public" or "unlisted").
|
|||
poetry run masto_gitbot
|
||||
# or
|
||||
# >>> poetry shell
|
||||
# >>> masto_gitbot
|
||||
# >>> masto_bridgebot
|
||||
# or however else you run python entrypoint scripts
|
||||
# hell you could do python -m masto_git_bridge.main:masto_gitbot
|
||||
# hell you could do python -m masto_bridges.main:masto_gitbot
|
||||
# i think?
|
||||
```
|
||||
|
||||
|
|
BIN
img/masto-caldav.png
Normal file
BIN
img/masto-caldav.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 402 KiB |
164
masto_bridges/bot.py
Normal file
164
masto_bridges/bot.py
Normal file
|
@ -0,0 +1,164 @@
|
|||
import pdb
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from masto_bridges.config import Config
|
||||
from masto_bridges.models import Account, List
|
||||
from masto_bridges.post import Post, Status
|
||||
from masto_bridges.logger import init_logger
|
||||
from masto_bridges.repo import Repo
|
||||
from masto_bridges.caldav import CalDAV
|
||||
|
||||
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 = None
|
||||
if self.config.ENABLE_GIT:
|
||||
self.repo = Repo(path=config.GIT_REPO)
|
||||
|
||||
self.caldav = None
|
||||
if self.config.ENABLE_CALDAV is not None:
|
||||
self.caldav = CalDAV(config=self.config)
|
||||
|
||||
|
||||
|
||||
def on_update(self, status:dict):
|
||||
try:
|
||||
status = Status(**status)
|
||||
except:
|
||||
self.logger.warning(f'Unhandled event: {status}')
|
||||
return
|
||||
|
||||
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 = False
|
||||
if self.repo:
|
||||
git_success = self.repo.post(post.format_commit())
|
||||
else:
|
||||
git_success = True
|
||||
|
||||
if self.caldav:
|
||||
caldav_success = self.caldav.post(post.format_caldav())
|
||||
else:
|
||||
caldav_success = False
|
||||
|
||||
success = git_success and caldav_success
|
||||
|
||||
if success:
|
||||
self.logger.info('Posted to bridges!')
|
||||
else:
|
||||
self.logger.exception('Failed to post to bridges!')
|
||||
|
||||
def on_unknown_event(self, name, unknown_event = None):
|
||||
"""An unknown mastodon API event has been received. The name contains the event-name and unknown_event
|
||||
contains the content of the unknown event.
|
||||
|
||||
This function must be implemented, if unknown events should be handled without an error.
|
||||
"""
|
||||
self.logger.exception(f'Unknown event type: {name}')
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Bot:
|
||||
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
|
||||
)
|
||||
|
||||
self.caldav = None
|
||||
if self.config.ENABLE_CALDAV:
|
||||
self.caldav = CalDAV(self.config)
|
||||
|
||||
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')
|
||||
if self.config.STREAM_MODE == 'list':
|
||||
self.client.stream_list(
|
||||
self.me_list.id,
|
||||
listener = listener,
|
||||
run_async=run_async
|
||||
)
|
||||
elif self.config.STREAM_MODE == 'home':
|
||||
self.client.stream_user(
|
||||
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}")
|
||||
|
||||
def poll_caldav(self):
|
||||
events = self.caldav.poll()
|
||||
if len(events) > 0:
|
||||
for event in events:
|
||||
post = Post.from_vevent(event.vobject_instance.vevent)
|
||||
if post.cal_user.split('@')[0] != self.me.acct and post.cal_user != self.me.acct:
|
||||
self.logger.debug(f"Not posting calendar event not from us. Got cal user: {post.cal_user} and we are {self.me.acct}")
|
||||
continue
|
||||
if post.cal_url:
|
||||
self.logger.debug("Not posting a post we made on masto back to masto")
|
||||
continue
|
||||
self.post(post.format_masto())
|
||||
|
||||
@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
|
99
masto_bridges/caldav.py
Normal file
99
masto_bridges/caldav.py
Normal file
|
@ -0,0 +1,99 @@
|
|||
from typing import Optional
|
||||
from caldav import DAVClient, Calendar
|
||||
from caldav.objects import SynchronizableCalendarObjectCollection
|
||||
# from caldav.lib.error import ResponseError
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
from icalendar import Event as iEvent
|
||||
from uuid import uuid1
|
||||
|
||||
from masto_bridges.config import Config
|
||||
|
||||
class Event(BaseModel):
|
||||
|
||||
summary: str # the title of the event, use for username
|
||||
dtstart: datetime
|
||||
dtend: datetime
|
||||
uid: str = Field(default_factory=uuid1)
|
||||
url: str = ''
|
||||
location:str = ''
|
||||
description:str = '' # post body
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def to_ical(self) -> str:
|
||||
evt = iEvent()
|
||||
evt.add('dtstart', self.dtstart)
|
||||
evt.add('dtend', self.dtend)
|
||||
evt.add('uid', self.uid)
|
||||
evt.add('location', self.location)
|
||||
evt.add('summary', self.summary)
|
||||
evt.add('description', self.description)
|
||||
evt.add('url', self.url)
|
||||
return evt.to_ical()
|
||||
|
||||
|
||||
class CalDAV:
|
||||
def __init__(self, config:Config):
|
||||
self.config = config
|
||||
for value in ('CALDAV_URL', 'CALDAV_USER', 'CALDAV_PASSWORD', 'CALDAV_CALENDAR_NAME'):
|
||||
if getattr(self.config, value) is None:
|
||||
raise ValueError('Need all CALDAV fields to be present in config! Check your .env file!')
|
||||
self._sync_token = None
|
||||
self._sync = None # type: Optional[SynchronizableCalendarObjectCollection]
|
||||
|
||||
|
||||
def get_calendar(self, name:str) -> Optional[Calendar]:
|
||||
"""
|
||||
Get a calendar by its name, creating one if none exists.
|
||||
"""
|
||||
with self.client as client:
|
||||
cals = client.principal().calendars()
|
||||
# try and find
|
||||
cal = [cal for cal in cals if cal.name == name]
|
||||
if len(cal) == 0:
|
||||
cal = client.principal().make_calendar(name=name)
|
||||
else:
|
||||
cal = cal[0]
|
||||
|
||||
return cal
|
||||
|
||||
def post(self, event:Event) -> bool:
|
||||
with self.client as client:
|
||||
calendar = self.get_calendar(self.config.CALDAV_CALENDAR_NAME)
|
||||
calendar.save_event(
|
||||
**event.dict()
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def poll(self) -> SynchronizableCalendarObjectCollection:
|
||||
cal = self.get_calendar(self.config.CALDAV_CALENDAR_NAME)
|
||||
if self._sync_token is None:
|
||||
# we haven't polled yet. get and ignore previous entries.
|
||||
# we don't want to post a bunch of junk every time lol
|
||||
self._sync = cal.objects(load_objects=False)
|
||||
|
||||
self._sync_token = self._sync.sync_token
|
||||
|
||||
try:
|
||||
self._sync = cal.objects(sync_token=self._sync_token, load_objects=True)
|
||||
self._sync_token = self._sync.sync_token
|
||||
except:
|
||||
# deleting calendar entries makes us choke!
|
||||
self._sync = cal.objects(sync_token=self._sync_token, load_objects=False)
|
||||
self._sync_token = self._sync.sync_token
|
||||
|
||||
return self._sync
|
||||
|
||||
|
||||
|
||||
@property
|
||||
def client(self) -> DAVClient:
|
||||
return DAVClient(
|
||||
url=self.config.CALDAV_URL,
|
||||
username=self.config.CALDAV_USER,
|
||||
password=self.config.CALDAV_PASSWORD
|
||||
)
|
24
masto_bridges/config.py
Normal file
24
masto_bridges/config.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from pathlib import Path
|
||||
from typing import Optional, Literal
|
||||
from pydantic import BaseSettings, AnyHttpUrl
|
||||
|
||||
class Config(BaseSettings):
|
||||
MASTO_URL:AnyHttpUrl
|
||||
MASTO_TOKEN:str
|
||||
LOGDIR:Path=Path().home() / '.mastobridge'
|
||||
LOGLEVEL:Literal['DEBUG','INFO','WARNING','ERROR','EXCEPTION']='INFO'
|
||||
STREAM_MODE:Literal['list', 'home'] = 'list'
|
||||
ENABLE_GIT:bool=False
|
||||
ENABLE_CALDAV:bool=False
|
||||
GIT_REPO:Optional[Path]=None
|
||||
GIT_REMOTE_URL:Optional[AnyHttpUrl]=None
|
||||
CALDAV_URL:Optional[AnyHttpUrl] = None
|
||||
CALDAV_USER:Optional[str]=None
|
||||
CALDAV_PASSWORD:Optional[str]=None
|
||||
CALDAV_CALENDAR_NAME:Optional[str]=None
|
||||
|
||||
|
||||
class Config:
|
||||
env_file = '.env'
|
||||
env_file_encoding = 'utf-8'
|
||||
env_prefix = "MASTOBRIDGE_"
|
|
@ -1,9 +1,10 @@
|
|||
import pdb
|
||||
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 masto_bridges.config import Config
|
||||
from masto_bridges.repo import Repo
|
||||
from masto_bridges.bot import Bot
|
||||
from masto_bridges.post import Post
|
||||
from masto_bridges.logger import init_logger
|
||||
from time import sleep
|
||||
|
||||
def post_last_commit(config:Optional[Config]=None):
|
||||
|
@ -27,7 +28,7 @@ def post_last_commit(config:Optional[Config]=None):
|
|||
bot = Bot(config=config)
|
||||
bot.post(post.format_masto())
|
||||
|
||||
def masto_gitbot(config:Optional[Config]=None):
|
||||
def masto_bridgebot(config:Optional[Config]=None):
|
||||
if config is None:
|
||||
config = Config()
|
||||
|
||||
|
@ -35,8 +36,17 @@ def masto_gitbot(config:Optional[Config]=None):
|
|||
try:
|
||||
bot.init_stream()
|
||||
while True:
|
||||
sleep(60*60)
|
||||
bot.logger.info('taking a breath')
|
||||
|
||||
sleep(1)
|
||||
if bot.caldav:
|
||||
# poll for new events
|
||||
bot.poll_caldav()
|
||||
# pdb.set_trace()
|
||||
|
||||
# print(events.objects)
|
||||
bot.logger.debug('taking a breath')
|
||||
except KeyboardInterrupt:
|
||||
bot.logger.info('quitting!')
|
||||
|
||||
|
||||
|
116
masto_bridges/post.py
Normal file
116
masto_bridges/post.py
Normal file
|
@ -0,0 +1,116 @@
|
|||
from typing import Optional, Literal, TYPE_CHECKING
|
||||
from datetime import datetime, timedelta
|
||||
from pydantic import BaseModel
|
||||
import re
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vobject.icalendar import RecurringComponent
|
||||
|
||||
from masto_bridges.repo import Commit
|
||||
from masto_bridges.models import Account
|
||||
from masto_bridges.caldav import Event
|
||||
|
||||
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']
|
||||
created_at:datetime
|
||||
in_reply_to_id: Optional[int] = None
|
||||
in_reply_to_account_id: Optional[int] = None
|
||||
spoiler_text: Optional[str] = None
|
||||
reblog: Optional['Status'] = None
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if kwargs.get('url', None) is None:
|
||||
# try using the uri
|
||||
kwargs['url'] = kwargs['uri']
|
||||
super(Status, self).__init__(**kwargs)
|
||||
|
||||
class Config:
|
||||
extra='ignore'
|
||||
|
||||
class Post(BaseModel):
|
||||
#timestamp: Optional[datetime] = None
|
||||
text:str
|
||||
status:Optional[Status] = None
|
||||
commit:Optional[Commit] = None
|
||||
cal_user:Optional[str] = None
|
||||
cal_url:Optional[str] = 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':
|
||||
# if this is a boost, get the original status
|
||||
if status.reblog:
|
||||
date = status.created_at
|
||||
status = status.reblog
|
||||
status.created_at = date
|
||||
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def from_vevent(cls, event:'RecurringComponent'):
|
||||
user=event.summary.value
|
||||
text = event.description.value
|
||||
try:
|
||||
url = event.location.value
|
||||
except AttributeError:
|
||||
url = None
|
||||
return Post(text=text, cal_user=user, cal_url=url)
|
||||
|
||||
|
||||
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}
|
||||
"""
|
||||
if self.commit is not None:
|
||||
return f"xpost from git-social: {self.commit.url}\n---\n{self.text}"""
|
||||
elif self.cal_user is not None:
|
||||
return f"xpost from a calendar\n---\n{self.text}"""
|
||||
else:
|
||||
return 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}"
|
||||
|
||||
def format_caldav(self) -> Event:
|
||||
"""
|
||||
Format a mastodon post for posting to CALDAV
|
||||
"""
|
||||
return Event(
|
||||
dtstart=self.status.created_at,
|
||||
dtend = self.status.created_at + timedelta(minutes=5),
|
||||
location = self.status.url,
|
||||
summary= self.status.account.acct,
|
||||
description = self.text
|
||||
)
|
|
@ -1,106 +0,0 @@
|
|||
import pdb
|
||||
from typing import Optional
|
||||
from datetime import datetime
|
||||
from masto_git_bridge.config import Config
|
||||
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, 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
|
|
@ -1,15 +0,0 @@
|
|||
from pathlib import Path
|
||||
from pydantic import BaseSettings, AnyHttpUrl
|
||||
|
||||
class Config(BaseSettings):
|
||||
MASTO_URL:AnyHttpUrl
|
||||
MASTO_TOKEN:str
|
||||
GIT_REPO:Path
|
||||
GIT_REMOTE_URL:AnyHttpUrl
|
||||
LOGDIR:Path=Path().home() / '.mastogit'
|
||||
|
||||
class Config:
|
||||
env_file = '.env'
|
||||
env_file_encoding = 'utf-8'
|
||||
env_prefix = "MASTOGIT_"
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
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: 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}"
|
560
poetry.lock
generated
560
poetry.lock
generated
|
@ -1,3 +1,5 @@
|
|||
# This file is automatically @generated by Poetry and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "beautifulsoup4"
|
||||
version = "4.11.1"
|
||||
|
@ -5,6 +7,10 @@ description = "Screen-scraping library"
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
files = [
|
||||
{file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"},
|
||||
{file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
soupsieve = ">1.2"
|
||||
|
@ -20,10 +26,38 @@ description = "Pure-Python implementation of the blurhash algorithm."
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "blurhash-1.1.4-py2.py3-none-any.whl", hash = "sha256:7611c1bc41383d2349b6129208587b5d61e8792ce953893cb49c38beeb400d1d"},
|
||||
{file = "blurhash-1.1.4.tar.gz", hash = "sha256:da56b163e5a816e4ad07172f5639287698e09d7f3dc38d18d9726d9c1dbc4cee"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["Pillow", "numpy", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "caldav"
|
||||
version = "1.2.1"
|
||||
description = "CalDAV (RFC4791) client library"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "caldav-1.2.1-py3-none-any.whl", hash = "sha256:6860abfab926e7a7290e94e2e179dd9ddb06ea6bc4eb60304b49ef4b14b85c6f"},
|
||||
{file = "caldav-1.2.1.tar.gz", hash = "sha256:934d7169d6e51b6e6ed4beef579b3d4008e2e6edac326a70225a32400ead86c9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
icalendar = "*"
|
||||
lxml = "*"
|
||||
pytz = "*"
|
||||
recurring-ical-events = ">=2.0.0"
|
||||
requests = "*"
|
||||
tzlocal = "*"
|
||||
vobject = "*"
|
||||
|
||||
[package.extras]
|
||||
test = ["coverage", "icalendar", "pytest", "pytest-coverage", "pytz", "radicale", "tzlocal", "xandikos"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2022.9.24"
|
||||
|
@ -31,6 +65,10 @@ description = "Python package for providing Mozilla's CA Bundle."
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
|
||||
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
|
@ -39,6 +77,10 @@ description = "The Real First Universal Charset Detector. Open, modern and activ
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6.0"
|
||||
files = [
|
||||
{file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"},
|
||||
{file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
unicode-backport = ["unicodedata2"]
|
||||
|
@ -50,6 +92,10 @@ description = "Python parser for the CommonMark Markdown spec"
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
|
||||
{file = "commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["flake8 (==3.7.8)", "hypothesis (==3.55.3)"]
|
||||
|
@ -61,6 +107,10 @@ description = "Decorators for Humans"
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"},
|
||||
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dnspython"
|
||||
|
@ -69,6 +119,10 @@ description = "DNS toolkit"
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6,<4.0"
|
||||
files = [
|
||||
{file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"},
|
||||
{file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
curio = ["curio (>=1.2,<2.0)", "sniffio (>=1.1,<2.0)"]
|
||||
|
@ -85,11 +139,31 @@ description = "A robust email address syntax and deliverability validation libra
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
files = [
|
||||
{file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"},
|
||||
{file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
dnspython = ">=1.15.0"
|
||||
idna = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "icalendar"
|
||||
version = "5.0.7"
|
||||
description = "iCalendar parser/generator"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "icalendar-5.0.7-py3-none-any.whl", hash = "sha256:18ad51f9d1741d33795ddaf5c886c59f6575f287d30e5a145c2d42ef72910c7f"},
|
||||
{file = "icalendar-5.0.7.tar.gz", hash = "sha256:e306014a64dc4dcf638da0acb2487ee4ada57b871b03a62ed7b513dfc135655c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = "*"
|
||||
pytz = "*"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.4"
|
||||
|
@ -97,6 +171,10 @@ description = "Internationalized Domain Names in Applications (IDNA)"
|
|||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
|
@ -105,214 +183,7 @@ description = "Powerful and Pythonic XML processing library combining libxml2/li
|
|||
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"
|
||||
description = "Python wrapper for the Mastodon API"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[package.dependencies]
|
||||
blurhash = ">=1.1.4"
|
||||
decorator = ">=4.0.0"
|
||||
python-dateutil = "*"
|
||||
python-magic = "*"
|
||||
pytz = "*"
|
||||
requests = ">=2.4.2"
|
||||
six = "*"
|
||||
|
||||
[package.extras]
|
||||
blurhash = ["blurhash (>=1.1.4)"]
|
||||
test = ["blurhash (>=1.1.4)", "cryptography (>=1.6.0)", "http-ece (>=1.0.5)", "pytest", "pytest-cov", "pytest-mock", "pytest-runner", "pytest-vcr", "requests-mock", "vcrpy"]
|
||||
webpush = ["cryptography (>=1.6.0)", "http-ece (>=1.0.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.10.2"
|
||||
description = "Data validation and settings management using python type hints"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""}
|
||||
python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""}
|
||||
typing-extensions = ">=4.1.0"
|
||||
|
||||
[package.extras]
|
||||
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"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "0.21.0"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-magic"
|
||||
version = "0.4.27"
|
||||
description = "File type identification using libmagic"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2022.6"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.28.1"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7, <4"
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = ">=2,<3"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
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"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
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"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.12"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
||||
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.9"
|
||||
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"},
|
||||
]
|
||||
certifi = [
|
||||
{file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"},
|
||||
{file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"},
|
||||
]
|
||||
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"},
|
||||
]
|
||||
dnspython = [
|
||||
{file = "dnspython-2.2.1-py3-none-any.whl", hash = "sha256:a851e51367fb93e9e1361732c1d60dab63eff98712e503ea7d92e6eccb109b4f"},
|
||||
{file = "dnspython-2.2.1.tar.gz", hash = "sha256:0f7569a4a6ff151958b64304071d370daa3243d15941a7beedf0c9fe5105603e"},
|
||||
]
|
||||
email-validator = [
|
||||
{file = "email_validator-1.3.0-py2.py3-none-any.whl", hash = "sha256:816073f2a7cffef786b29928f58ec16cdac42710a53bb18aa94317e3e145ec5c"},
|
||||
{file = "email_validator-1.3.0.tar.gz", hash = "sha256:553a66f8be2ec2dea641ae1d3f29017ab89e9d603d4a25cdaac39eefa283d769"},
|
||||
]
|
||||
idna = [
|
||||
{file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
|
||||
{file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
|
||||
]
|
||||
lxml = [
|
||||
files = [
|
||||
{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"},
|
||||
|
@ -384,11 +255,47 @@ lxml = [
|
|||
{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 = [
|
||||
|
||||
[package.extras]
|
||||
cssselect = ["cssselect (>=0.7)"]
|
||||
html5 = ["html5lib"]
|
||||
htmlsoup = ["BeautifulSoup4"]
|
||||
source = ["Cython (>=0.29.7)"]
|
||||
|
||||
[[package]]
|
||||
name = "mastodon-py"
|
||||
version = "1.5.2"
|
||||
description = "Python wrapper for the Mastodon API"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{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"},
|
||||
]
|
||||
pydantic = [
|
||||
|
||||
[package.dependencies]
|
||||
blurhash = ">=1.1.4"
|
||||
decorator = ">=4.0.0"
|
||||
python-dateutil = "*"
|
||||
python-magic = "*"
|
||||
pytz = "*"
|
||||
requests = ">=2.4.2"
|
||||
six = "*"
|
||||
|
||||
[package.extras]
|
||||
blurhash = ["blurhash (>=1.1.4)"]
|
||||
test = ["blurhash (>=1.1.4)", "cryptography (>=1.6.0)", "http-ece (>=1.0.5)", "pytest", "pytest-cov", "pytest-mock", "pytest-runner", "pytest-vcr", "requests-mock", "vcrpy"]
|
||||
webpush = ["cryptography (>=1.6.0)", "http-ece (>=1.0.5)"]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.10.2"
|
||||
description = "Data validation and settings management using python type hints"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pydantic-1.10.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bb6ad4489af1bac6955d38ebcb95079a836af31e4c4f74aba1ca05bb9f6027bd"},
|
||||
{file = "pydantic-1.10.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1f5a63a6dfe19d719b1b6e6106561869d2efaca6167f84f5ab9347887d78b98"},
|
||||
{file = "pydantic-1.10.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:352aedb1d71b8b0736c6d56ad2bd34c6982720644b0624462059ab29bd6e5912"},
|
||||
|
@ -426,47 +333,258 @@ pydantic = [
|
|||
{file = "pydantic-1.10.2-py3-none-any.whl", hash = "sha256:1b6ee725bd6e83ec78b1aa32c5b1fa67a3a65badddde3976bca5fe4568f27709"},
|
||||
{file = "pydantic-1.10.2.tar.gz", hash = "sha256:91b8e218852ef6007c2b98cd861601c6a09f1aa32bbbb74fab5b1c33d4a1e410"},
|
||||
]
|
||||
pygments = [
|
||||
|
||||
[package.dependencies]
|
||||
email-validator = {version = ">=1.0.3", optional = true, markers = "extra == \"email\""}
|
||||
python-dotenv = {version = ">=0.10.4", optional = true, markers = "extra == \"dotenv\""}
|
||||
typing-extensions = ">=4.1.0"
|
||||
|
||||
[package.extras]
|
||||
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"
|
||||
files = [
|
||||
{file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"},
|
||||
{file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"},
|
||||
]
|
||||
python-dateutil = [
|
||||
|
||||
[package.extras]
|
||||
plugins = ["importlib-metadata"]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.8.2"
|
||||
description = "Extensions to the standard Python datetime module"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||
files = [
|
||||
{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"},
|
||||
]
|
||||
python-dotenv = [
|
||||
|
||||
[package.dependencies]
|
||||
six = ">=1.5"
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "0.21.0"
|
||||
description = "Read key-value pairs from a .env file and set them as environment variables"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "python-dotenv-0.21.0.tar.gz", hash = "sha256:b77d08274639e3d34145dfa6c7008e66df0f04b7be7a75fd0d5292c191d79045"},
|
||||
{file = "python_dotenv-0.21.0-py3-none-any.whl", hash = "sha256:1684eb44636dd462b66c3ee016599815514527ad99965de77f43e0944634a7e5"},
|
||||
]
|
||||
python-magic = [
|
||||
|
||||
[package.extras]
|
||||
cli = ["click (>=5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "python-magic"
|
||||
version = "0.4.27"
|
||||
description = "File type identification using libmagic"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
files = [
|
||||
{file = "python-magic-0.4.27.tar.gz", hash = "sha256:c1ba14b08e4a5f5c31a302b7721239695b2f0f058d125bd5ce1ee36b9d9d3c3b"},
|
||||
{file = "python_magic-0.4.27-py2.py3-none-any.whl", hash = "sha256:c212960ad306f700aa0d01e5d7a325d20548ff97eb9920dcd29513174f0294d3"},
|
||||
]
|
||||
pytz = [
|
||||
|
||||
[[package]]
|
||||
name = "pytz"
|
||||
version = "2022.6"
|
||||
description = "World timezone definitions, modern and historical"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"},
|
||||
{file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"},
|
||||
]
|
||||
requests = [
|
||||
|
||||
[[package]]
|
||||
name = "recurring-ical-events"
|
||||
version = "2.0.2"
|
||||
description = "A Python module which repeats ICalendar events by RRULE, RDATE and EXDATE."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "recurring_ical_events-2.0.2-py3-none-any.whl", hash = "sha256:a618140129e2ff00afa6b7afc1a154c2cb2177166621a519941b7058d6f6c339"},
|
||||
{file = "recurring_ical_events-2.0.2.tar.gz", hash = "sha256:c4bc7d0d26fcda0ac3b773c9706fd1e271b7b0e0230ad0ee02ccab55e274ea11"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
icalendar = "*"
|
||||
python-dateutil = ">=2.8.1"
|
||||
pytz = "*"
|
||||
x-wr-timezone = ">=0.0.5,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.28.1"
|
||||
description = "Python HTTP for Humans."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7, <4"
|
||||
files = [
|
||||
{file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
|
||||
{file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
|
||||
]
|
||||
rich = [
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=2017.4.17"
|
||||
charset-normalizer = ">=2,<3"
|
||||
idna = ">=2.5,<4"
|
||||
urllib3 = ">=1.21.1,<1.27"
|
||||
|
||||
[package.extras]
|
||||
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"
|
||||
files = [
|
||||
{file = "rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"},
|
||||
{file = "rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"},
|
||||
]
|
||||
six = [
|
||||
|
||||
[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"
|
||||
description = "Python 2 and 3 compatibility utilities"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||
files = [
|
||||
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
|
||||
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
|
||||
]
|
||||
soupsieve = [
|
||||
|
||||
[[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"
|
||||
files = [
|
||||
{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 = [
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.4.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
||||
]
|
||||
urllib3 = [
|
||||
|
||||
[[package]]
|
||||
name = "tzdata"
|
||||
version = "2023.3"
|
||||
description = "Provider of IANA time zone data"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2"
|
||||
files = [
|
||||
{file = "tzdata-2023.3-py2.py3-none-any.whl", hash = "sha256:7e65763eef3120314099b6939b5546db7adce1e7d6f2e179e3df563c70511eda"},
|
||||
{file = "tzdata-2023.3.tar.gz", hash = "sha256:11ef1e08e54acb0d4f95bdb1be05da659673de4acbd21bf9c69e94cc5e907a3a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.0.1"
|
||||
description = "tzinfo object for the local timezone"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tzlocal-5.0.1-py3-none-any.whl", hash = "sha256:f3596e180296aaf2dbd97d124fe76ae3a0e3d32b258447de7b939b3fd4be992f"},
|
||||
{file = "tzlocal-5.0.1.tar.gz", hash = "sha256:46eb99ad4bdb71f3f72b7d24f4267753e240944ecfc16f25d2719ba89827a803"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
tzdata = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[package.extras]
|
||||
devenv = ["black", "check-manifest", "flake8", "pyroma", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.12"
|
||||
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
|
||||
files = [
|
||||
{file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"},
|
||||
{file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"]
|
||||
secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"]
|
||||
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "vobject"
|
||||
version = "0.9.6.1"
|
||||
description = "A full-featured Python package for parsing and creating iCalendar and vCard files"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "vobject-0.9.6.1.tar.gz", hash = "sha256:96512aec74b90abb71f6b53898dd7fe47300cc940104c4f79148f0671f790101"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
python-dateutil = ">=2.4.0"
|
||||
|
||||
[[package]]
|
||||
name = "x-wr-timezone"
|
||||
version = "0.0.5"
|
||||
description = "A Python module and program to convert calendars using X-WR-TIMEZONE to standard ones."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "x_wr_timezone-0.0.5-py3-none-any.whl", hash = "sha256:e438b27b96635f5f712a4fb5dda4c82597a53a412fe834c9fe8409fddb3fc2b1"},
|
||||
{file = "x_wr_timezone-0.0.5.tar.gz", hash = "sha256:c05cb34b9b58a4607a788db086dcae5766728e4b94e0672870dc5593a6e13fe6"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
icalendar = "*"
|
||||
pytz = "*"
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.9"
|
||||
content-hash = "c5dd090c8750013b4ea3a1c79ede4226cc28f5ea999544d29f6021344754e459"
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
[tool.poetry]
|
||||
name = "masto-gitsocial-bridge"
|
||||
name = "masto-bridges"
|
||||
version = "0.1.0"
|
||||
description = "Crosspost between mastodon and git.social"
|
||||
description = "Crosspost between mastodon and git.social and also CalDAV for some reason"
|
||||
authors = ["sneakers-the-rat <JLSaunders987@gmail.com>"]
|
||||
license = "AGPL-3.0"
|
||||
readme = "README.md"
|
||||
packages = [{include = "masto_git_bridge"}]
|
||||
packages = [{include = "masto_bridges"}]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9"
|
||||
|
@ -14,10 +14,12 @@ pydantic = {extras = ["dotenv", "email"], version = "^1.10.2"}
|
|||
rich = "^12.6.0"
|
||||
beautifulsoup4 = "^4.11.1"
|
||||
lxml = "^4.9.1"
|
||||
caldav = "^1.2.1"
|
||||
icalendar = "^5.0.7"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
post_last_commit = 'masto_git_bridge.main:post_last_commit'
|
||||
masto_gitbot = 'masto_git_bridge.main:masto_gitbot'
|
||||
post_last_commit = 'masto_bridges.main:post_last_commit'
|
||||
masto_bridgebot = 'masto_bridges.main:masto_bridgebot'
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
|
Loading…
Reference in a new issue