2022-08-24 08:09:41 +03:00
|
|
|
#!/usr/bin/env python3
|
2023-05-08 13:18:44 +03:00
|
|
|
"""
|
|
|
|
mastoposter - configurable reposter from Mastodon-compatible Fediverse servers
|
|
|
|
Copyright (C) 2022-2023 hatkidchan <hatkidchan@gmail.com>
|
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation; either version 3 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
"""
|
2023-05-10 05:28:21 +03:00
|
|
|
from argparse import ArgumentParser
|
2022-08-24 08:09:41 +03:00
|
|
|
from asyncio import run
|
2022-08-31 18:12:38 +03:00
|
|
|
from configparser import ConfigParser, ExtendedInterpolation
|
2023-01-26 13:44:30 +03:00
|
|
|
from logging import (
|
|
|
|
INFO,
|
|
|
|
Formatter,
|
|
|
|
Logger,
|
|
|
|
StreamHandler,
|
|
|
|
getLevelName,
|
|
|
|
getLogger,
|
|
|
|
)
|
2023-05-10 05:28:21 +03:00
|
|
|
from os import getenv
|
|
|
|
from sys import stdout
|
2022-08-26 14:32:55 +03:00
|
|
|
from typing import AsyncGenerator, Callable, List
|
2023-05-10 05:28:21 +03:00
|
|
|
|
2023-03-07 10:26:45 +03:00
|
|
|
from httpx import Client, HTTPTransport
|
2022-09-14 20:29:23 +03:00
|
|
|
|
2023-05-10 05:28:21 +03:00
|
|
|
from mastoposter import (
|
|
|
|
execute_integrations,
|
|
|
|
load_integrations_from,
|
|
|
|
__version__,
|
|
|
|
__description__
|
|
|
|
)
|
|
|
|
from mastoposter.integrations import FilteredIntegration
|
|
|
|
from mastoposter.sources import websocket_source
|
|
|
|
from mastoposter.types import Account, Status
|
2022-11-01 12:55:23 +03:00
|
|
|
from mastoposter.utils import normalize_config
|
|
|
|
|
2022-09-14 20:29:23 +03:00
|
|
|
|
|
|
|
WSOCK_TEMPLATE = "wss://{instance}/api/v1/streaming"
|
2022-09-18 16:59:19 +03:00
|
|
|
VERIFY_CREDS_TEMPLATE = "https://{instance}/api/v1/accounts/verify_credentials"
|
2022-08-24 08:09:41 +03:00
|
|
|
|
2022-11-01 12:55:23 +03:00
|
|
|
logger = getLogger()
|
|
|
|
|
2022-08-24 08:09:41 +03:00
|
|
|
|
2022-11-01 13:52:40 +03:00
|
|
|
def init_logger(loglevel: int = INFO):
|
2022-11-01 13:04:31 +03:00
|
|
|
stdout_handler = StreamHandler(stdout)
|
2022-11-01 13:52:40 +03:00
|
|
|
stdout_handler.setLevel(loglevel)
|
2022-11-01 13:04:31 +03:00
|
|
|
formatter = Formatter("[%(asctime)s][%(levelname)5s:%(name)s] %(message)s")
|
|
|
|
stdout_handler.setFormatter(formatter)
|
|
|
|
logger.addHandler(stdout_handler)
|
2022-11-01 13:37:47 +03:00
|
|
|
logger.setLevel(loglevel)
|
2023-01-26 13:44:30 +03:00
|
|
|
for log in logger.manager.loggerDict.values():
|
|
|
|
if isinstance(log, Logger):
|
|
|
|
log.setLevel(loglevel)
|
2022-11-01 13:04:31 +03:00
|
|
|
|
|
|
|
|
2022-08-24 08:09:41 +03:00
|
|
|
async def listen(
|
|
|
|
source: Callable[..., AsyncGenerator[Status, None]],
|
2022-08-29 10:28:51 +03:00
|
|
|
drains: List[FilteredIntegration],
|
2022-08-24 08:09:41 +03:00
|
|
|
user: str,
|
|
|
|
/,
|
|
|
|
**kwargs,
|
|
|
|
):
|
2022-11-01 12:55:23 +03:00
|
|
|
logger.info("Starting listening...")
|
2022-08-24 08:09:41 +03:00
|
|
|
async for status in source(**kwargs):
|
2022-11-01 14:33:47 +03:00
|
|
|
logger.info("New status: %s", status.uri)
|
2022-11-01 12:55:23 +03:00
|
|
|
logger.debug("Got status: %r", status)
|
2022-08-24 08:09:41 +03:00
|
|
|
if status.account.id != user:
|
2022-11-01 13:52:40 +03:00
|
|
|
logger.info(
|
|
|
|
"Skipping status %s (account.id=%r != %r)",
|
|
|
|
status.uri,
|
|
|
|
status.account.id,
|
|
|
|
user,
|
|
|
|
)
|
2022-08-24 08:09:41 +03:00
|
|
|
continue
|
2022-08-26 02:03:06 +03:00
|
|
|
|
|
|
|
# TODO: add option/filter to handle that
|
2022-09-01 00:57:36 +03:00
|
|
|
if status.visibility in ("direct",):
|
2022-11-01 12:55:23 +03:00
|
|
|
logger.info(
|
|
|
|
"Skipping post %s (status.visibility=%r)",
|
|
|
|
status.uri,
|
|
|
|
status.visibility,
|
|
|
|
)
|
2022-08-24 08:09:41 +03:00
|
|
|
continue
|
2022-08-26 02:03:06 +03:00
|
|
|
|
|
|
|
# TODO: find a better way to handle threads
|
2022-08-24 08:09:41 +03:00
|
|
|
if (
|
|
|
|
status.in_reply_to_account_id is not None
|
|
|
|
and status.in_reply_to_account_id != user
|
|
|
|
):
|
2022-11-01 12:55:23 +03:00
|
|
|
logger.info(
|
|
|
|
"Skipping post %s because it's a reply to another person",
|
|
|
|
status.uri,
|
|
|
|
)
|
2022-08-24 08:09:41 +03:00
|
|
|
continue
|
2022-08-26 02:03:06 +03:00
|
|
|
|
2022-08-26 18:37:36 +03:00
|
|
|
await execute_integrations(status, drains)
|
2022-08-24 08:09:41 +03:00
|
|
|
|
|
|
|
|
2023-05-10 05:28:21 +03:00
|
|
|
def main():
|
|
|
|
parser = ArgumentParser(
|
|
|
|
prog="mastoposter",
|
|
|
|
description=__description__
|
|
|
|
)
|
|
|
|
parser.add_argument("config", nargs="?",
|
|
|
|
default=getenv("MASTOPOSTER_CONFIG_FILE")
|
|
|
|
)
|
|
|
|
parser.add_argument("-v", action="version", version=__version__)
|
|
|
|
args = parser.parse_args()
|
2022-08-24 08:09:41 +03:00
|
|
|
|
2023-05-10 05:28:21 +03:00
|
|
|
if not args.config:
|
|
|
|
raise RuntimeError("No config file. Aborting")
|
|
|
|
|
|
|
|
conf = ConfigParser(interpolation=ExtendedInterpolation())
|
|
|
|
conf.read(args.config)
|
2022-11-02 20:11:38 +03:00
|
|
|
init_logger(getLevelName(conf["main"].get("loglevel", "INFO")))
|
2022-11-01 12:55:23 +03:00
|
|
|
normalize_config(conf)
|
2022-08-26 02:03:06 +03:00
|
|
|
|
2022-08-29 10:28:51 +03:00
|
|
|
modules: List[FilteredIntegration] = load_integrations_from(conf)
|
2023-03-07 10:26:45 +03:00
|
|
|
retries: int = conf["main"].getint("http-retries", 5)
|
2022-08-24 08:09:41 +03:00
|
|
|
|
2022-11-01 12:55:23 +03:00
|
|
|
logger.info("Loaded %d integrations", len(modules))
|
|
|
|
|
2022-09-14 20:29:23 +03:00
|
|
|
user_id: str = conf["main"]["user"]
|
|
|
|
if user_id == "auto":
|
2022-11-01 12:55:23 +03:00
|
|
|
logger.info("config.main.user is set to auto, getting user ID")
|
2023-03-07 10:26:45 +03:00
|
|
|
with Client(transport=HTTPTransport(retries=retries)) as c:
|
2022-09-14 20:29:23 +03:00
|
|
|
rq = c.get(
|
|
|
|
VERIFY_CREDS_TEMPLATE.format(**conf["main"]),
|
|
|
|
params={"access_token": conf["main"]["token"]},
|
|
|
|
)
|
|
|
|
account = Account.from_dict(rq.json())
|
|
|
|
user_id = account.id
|
2022-11-01 14:33:47 +03:00
|
|
|
|
|
|
|
logger.info("account.id=%s", user_id)
|
2022-09-14 20:29:23 +03:00
|
|
|
|
2022-08-24 08:09:41 +03:00
|
|
|
url = "wss://{}/api/v1/streaming".format(conf["main"]["instance"])
|
2022-11-01 12:55:23 +03:00
|
|
|
|
2022-08-24 08:09:41 +03:00
|
|
|
run(
|
|
|
|
listen(
|
|
|
|
websocket_source,
|
|
|
|
modules,
|
2022-09-14 20:29:23 +03:00
|
|
|
user_id,
|
2022-08-24 08:09:41 +03:00
|
|
|
url=url,
|
2022-08-27 14:27:42 +03:00
|
|
|
reconnect=conf["main"].getboolean("auto_reconnect", False),
|
2023-03-07 10:26:45 +03:00
|
|
|
reconnect_delay=conf["main"].getfloat("reconnect_delay", 1.0),
|
2022-08-24 08:09:41 +03:00
|
|
|
list=conf["main"]["list"],
|
|
|
|
access_token=conf["main"]["token"],
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2023-05-08 17:04:32 +03:00
|
|
|
main()
|