예제 #1
0
파일: bot.py 프로젝트: csmall/420bot
def main():
    clientcred_file = BOT_NAME + '_clientcred.secret'
    usercred_file = BOT_NAME + '_usercred.secret'

    args = parse_args()

    if args.register:
        register(args, clientcred_file, usercred_file)
        sys.exit(0)

    mastodon = Mastodon(client_id=clientcred_file,
                        access_token=usercred_file,
                        api_base_url=BASE_URL)

    instance = mastodon.instance()
    print('Successfully logged into instance "{0}".'.format(instance.title))
    run_bot(mastodon)
예제 #2
0
파일: __main__.py 프로젝트: absucc/mastobot
class Bot:
    def __init__(self,
                 instance_url: str,
                 access_token: str,
                 websocket_mode=False):
        """Intiate a Mastodon bot.

        :param instance_url: (str) base URL for your Mastodon instance of choice,
            e.g. ``https://mastodon.technology``.
        :param access_token: (str) "Your access token" inside
            Preferences -> Development -> some application.
        :param websocket_mode: (bool) whether to use websockets for streaming
        """
        self._instance = instance_url
        self._token = access_token
        self._handle = ""  # will be like "*****@*****.**"
        self._atname = ""  # will be like "@bot"
        self._triggers = []
        self._websocket_mode = websocket_mode
        self._check_update_triggers = lambda o: self._check_triggers(o, UPDATE)
        self._check_notification_triggers = lambda o: self._check_triggers(
            o, NOTIFICATION)

    def _check_triggers(self, obj: dict, stream: str):
        """Handle events from ``mastodon.StreamListener.on_update()`` if 
            ``stream=="update"``, and ``mastodon.StreamListener.on_notification()``
            if ``stream=="notification"``.
        """
        # set up expectations
        if stream == UPDATE:
            status = obj
            event = UPDATE
        elif stream == NOTIFICATION:
            status = obj["status"]
            event = obj["type"]
        else:
            return
        # if the bot is mentioned, remove the mention text from status content
        # before testing trigger
        mentioned_accts = [m["acct"] for m in status["mentions"]]
        if self._handle in mentioned_accts:
            to_be_removed = self._atname
        else:
            to_be_removed = ""
        for trig in self._triggers:
            if not trig.event == event:
                continue
            if trig.test(
                    event,
                    html_to_text(status["content"]).replace(
                        to_be_removed, "", 1).strip(),
            ):
                # pass obj to trig. trig will decide which elements
                # to pass on to bot-developer-defined callback.
                reply = trig.invoke(obj)
                if reply:
                    self._respond(status, reply)

    def _respond(self, status: dict, content):
        """Reply to a status with content, boost, or favourite it.

        :param status: (dict) the status to respond to.
        :param content: When ``content`` is a string,
            simply reply with it, keeping everything else as Mastodon.py decides.
            When it is an instance of the ``mastobot.Reply`` class, all its arguments
            will be passed on to Mastodon.py. The rest are left in their default state.
            When it is ``mastobot.Boost``, boost ``status``. Ditto for ``Favourite``.
            When it is a list/tuple, recursively call ``self._respond(status, n)``
            for each ``n`` in content.
        """
        if not content:
            raise ValueError(f"Response to {status['id']} empty; aborted")

        if type(content) in (list, tuple):
            for n in content:
                self._respond(status, n)
            return
        elif content == Boost:
            self._bot.status_reblog(status["id"])
            return
        elif content == Favourite:
            self._bot.status_favourite(status["id"])
            return
        elif type(content) == str:
            args = {
                STATUS: content,
                VISIBILITY: status[VISIBILITY],
                SPOILER_TEXT: status[SPOILER_TEXT],
            }
        elif isinstance(content, Reply):
            args = {
                STATUS: content.text,
                VISIBILITY: content.visibility or status[VISIBILITY],
                SPOILER_TEXT: content.spoiler_text or status[SPOILER_TEXT],
            }

        self._bot.status_post(in_reply_to_id=status["id"], **args)

    # decorator generators

    def on_mention(self, expectation, validation=EQUALS, case_sensitive=False):
        """Listen to mentions and invoke a callback with a ``Status`` object
            as argument.

        :param expectation: (str or callable) string, regex string or callable
            that evaluates to True if the status content is what you want.
        :param validation: (str) may be "equals", "contains", "regex" or "evaluate".
        """
        def decorator(callback):
            self._triggers.append(
                Trigger(
                    event=MENTION,
                    validation=validation,
                    expectation=expectation,
                    callback=callback,
                    case_sensitive=case_sensitive,
                ))

        return decorator

    def on_home_update(self,
                       expectation,
                       validation=EQUALS,
                       case_sensitive=False):
        """Listen to updates on the home timeline and invoke a callback with
            a ``Status`` object as argument.

        :param expectation: (str or callable) string, regex string or callable
            that evaluates to True if the status content is what you want.
        :param validation: (str) may be "equals", "contains", "regex" or "evaluate".
        """
        def decorator(callback):
            self._triggers.append(
                Trigger(
                    event=UPDATE,
                    validation=validation,
                    expectation=expectation,
                    callback=callback,
                    case_sensitive=case_sensitive,
                ))

        return decorator

    # execution

    def run(self):
        """Start bot.

        After all listeners (triggers) are set, invoke ``bot.run()``.
        """
        self._bot = Mastodon(api_base_url=self._instance,
                             access_token=self._token)
        print("Connected to " + self._instance)

        # keep a record of what this bot is called
        # so that "@atname" can be removed when necessary.
        me = self._bot.account_verify_credentials()
        self._handle = me["acct"]
        self._atname = "@" + me["username"]

        # register stream listeners
        if not self._websocket_mode:
            # use Mastodon.py StreamListener
            self._user_stream = StreamListener()
            self._user_stream.on_update = self._check_update_triggers
            self._user_stream.on_notification = self._check_notification_triggers
            self._bot.stream_user(self._user_stream)
        else:
            # use ws interface I implemented myself
            self._user_stream = WebsocketListener(self._bot.instance(),
                                                  self._token,
                                                  stream="user")
            self._user_stream.on_update = self._check_update_triggers
            self._user_stream.on_notification = self._check_notification_triggers
            self._user_stream.start_stream()
예제 #3
0
#      'pytooterapp',
#      api_base_url = 'https://mstdn.jp',
#      to_file = 'pytooter_clientcred.secret'
# )

# Log in - either every time, or use persisted

mastodon = Mastodon(client_id='pytooter_clientcred.secret',
                    api_base_url='https://mstdn.jp')
mastodon.log_in('your-registered-email-address',
                'your-password',
                to_file='pytooter_usercred.secret')

# Create actual API instance
mastodon = Mastodon(client_id='pytooter_clientcred.secret',
                    client_secret=None,
                    access_token='pytooter_usercred.secret',
                    api_base_url='https://mstdn.jp',
                    debug_requests=False,
                    ratelimit_method='pace',
                    ratelimit_pacefactor=1.1,
                    request_timeout=300,
                    mastodon_version=None,
                    version_check_mode='none')

description = mastodon.instance()

user_count = description['stats']['user_count']

# /api/v1/accounts/:id/followers
예제 #4
0
import sys
from mastodon import Mastodon

url = sys.argv[1]
cid_file = 'client_id.txt'
token_file = 'access_token.txt'

mastodon = Mastodon(client_id=cid_file,
                    access_token=token_file,
                    api_base_url=url)

# インスタンスのドメイン名を表示する
instance = mastodon.instance()
print(instance['uri'])
예제 #5
0
class PineappleBot(StreamListener):
    """
    Main bot class
    We subclass StreamListener so that we can use it as its own callback
    """

    INITIALIZING = 0
    STARTING = 1
    RUNNING = 2
    STOPPING = 3

    class Config(dict):
        def __init__(self, bot, filename): 
            dict.__init__(self)
            self._filename = filename
            self._cfg = ConfigObj(filename, create_empty=True, interpolation="configparser")
            self._bot = bot
        def __getattr__(self, key): 
            if self[key]:
                return self[key]
            else:
                warnings.warn("The {} setting does not appear in {}. Setting the self.config value to None.".format(key, self._filename), 
                        RuntimeWarning, 1)
                return None
        def __setattr__(self, key, value): self[key] = value
        def __delattr(self, key): del self[key]

        def load(self, name=None):
            """ Load section <name> from the config file into this object,
            replacing/overwriting any values currently cached in here."""
            if (name != None):
                self._name = name

            self._cfg.reload()
            if (name not in self._cfg.sections): 
                self._bot.log("config", "Section {} not in {}, aborting.".format(self._name, self._filename))
                return False
            self._bot.log("config", "Loading configuration from {}".format(self._filename))
            if "DEFAULT" in self._cfg.sections:
                self.update(self._cfg["DEFAULT"])
            self.update(self._cfg[self._name])
            return True

        def save(self):
            """ Save back out to the config file. """
            self._cfg.reload()
            for attr, value in self.items():
                if attr[0] != '_':
                    if "DEFAULT" not in self._cfg or (not (attr in self._cfg["DEFAULT"] and self._cfg["DEFAULT"][attr] == value)):
                        if isinstance(value, (list, tuple)):
                            value = ",".join([str(v) for v in value])
                    self._cfg[self._name][attr] = str(value)
            self._bot.log("config", "Saving configuration to {}...".format(self._filename))
            self._cfg.write()
            self._bot.log("config", "Done.")
            return True

    def __init__(self, cfgname, name=None, log_to_stderr=True, interactive=False, verbose=False):
        if (name is None): name = self.__class__.__name__
        self.name = name
        self.state = PineappleBot.INITIALIZING

        self.alive = threading.Condition()
        self.threads = []
        self.reply_funcs = []
        self.report_funcs = []

        self.mastodon = None
        self.account_info = None
        self.username = None
        self.default_visibility = None
        self.default_sensitive = None
        self.stream = None
        self.interactive = interactive
        self.verbose = verbose

        self.log_to_stderr = log_to_stderr
        self.log_name = self.name + ".log"
        self.log_file = open(self.log_name, "a")

        self.config = PineappleBot.Config(self, cfgname)
        self.init() # Call user init to initialize bot-specific properties to default values
        if not self.config.load(self.name): return
        if not self.login(): return

        self.startup()

    def log(self, id, msg):
        if (id == None): id = self.name
        else: id = self.name + "." + id
        ts = datetime.now()
        msg_f = "[{0:%Y-%m-%d %H:%M:%S}] {1}: {2}".format(ts, id, msg)

        if self.log_file.closed or self.log_to_stderr: print(msg_f, file=sys.stderr)
        elif not self.log_file.closed: print(msg_f, file=self.log_file)

    def startup(self):
        self.state = PineappleBot.STARTING
        self.log(None, "Starting {0} {1}".format(self.__class__.__name__, self.name))

        try:
            self.start()
        except Exception as e:
            self.log(None, "Fatal exception: {}\n{}".format(repr(e), traceback.format_exc()))
            return
        
        def interval_threadproc(f):
            self.log(f.__name__, "Started")
            t = datetime.now()
            tLast = t
            while (True):
                self.alive.acquire()
                t = datetime.now()
                interval = interval_next(f, t, tLast)

                if (interval == 0):
                    try:
                        f()
                    except Exception as e:
                        error = "Exception encountered in @interval function: {}\n{}".format(repr(e), traceback.format_exc())
                        self.report_error(error, f.__name__)

                    t = datetime.now()
                    interval = interval_next(f, t, t)

                if self.verbose: self.log(f.__name__ + ".debug", "Next wait interval: {}s".format(interval))
                tLast = t
                self.alive.wait(max(interval, 1))
                if (self.state == PineappleBot.STOPPING):
                    self.alive.release()
                    self.log(f.__name__, "Shutting down")
                    return 0
                else:
                    self.alive.release()

        for fname, f in inspect.getmembers(self, predicate=inspect.ismethod):
            if hasattr(f, "interval") or hasattr(f, "schedule"):
                t = threading.Thread(args=(f,), target=interval_threadproc)
                t.start()
                self.threads.append(t)

            if hasattr(f, "reply"):
                self.reply_funcs.append(f)

            if hasattr(f, "error_reporter"):
                self.report_funcs.append(f)

        if len(self.reply_funcs) > 0:
            self.stream = self.mastodon.stream_user(self, async=True)

        credentials = self.mastodon.account_verify_credentials()
        self.account_info = credentials
        self.username = credentials["username"]
        self.default_visibility = credentials["source"]["privacy"]
        self.default_sensitive = credentials["source"]["sensitive"]

        self.state = PineappleBot.RUNNING
        self.log(None, "Startup complete.")

    def shutdown(self):
        self.alive.acquire()
        self.state = PineappleBot.STOPPING
        self.log(None, "Stopping {0} {1}".format(self.__class__.__name__, self.name))
        self.alive.notify_all()
        self.alive.release()

        if self.stream: self.stream.close()

        self.stop()
        self.config.save()

        self.log_file.close()

    def login(self):
        if self.interactive and not self.interactive_login(): 
            self.log("api", "Interactive login failed, exiting.")
            return False
        elif "domain" not in self.config:
            self.log("api", "No domain set in config and interactive = False, exiting.")
            return False
        elif "client_id" not in self.config:
            self.log("api", "No client id set in config and interactive = False, exiting.")
            return False
        elif "client_secret" not in self.config:
            self.log("api", "No client secret set in config and interactive = False, exiting.")
            return False
        elif "access_token" not in self.config:
            self.log("api", "No access key set in config and interactive = False, exiting.")
            return False

        self.mastodon = Mastodon(client_id = self.config.client_id, 
                                  client_secret = self.config.client_secret, 
                                  access_token = self.config.access_token, 
                                  api_base_url = self.config.domain)
                                  #debug_requests = True)
        return True

    def interactive_login(self):
        try:
            if (not hasattr(self, "domain")):
                domain = input("{0}: Enter the instance domain [mastodon.social]: ".format(self.name))
                domain = domain.strip()
                if (domain == ""): domain = "mastodon.social"
                self.config.domain = domain
            # We have to generate these two together, if just one is 
            # specified in the config file it's no good.
            if (not hasattr(self, "client_id") or not hasattr(self, "client_secret")):
                client_name = input("{0}: Enter a name for this bot or service [{0}]: ".format(self.name))
                client_name = client_name.strip()
                if (client_name == ""): client_name = self.name
                self.config.client_id, self.config.client_secret = Mastodon.create_app(client_name, 
                        api_base_url="https://"+self.config.domain)
                # TODO handle failure
                self.config.save()
            if (not hasattr(self, "access_token")):
                email = input("{0}: Enter the account email: ".format(self.name))
                email = email.strip()
                password = getpass.getpass("{0}: Enter the account password: "******"https://"+self.config.domain)
                    self.config.access_token = mastodon.log_in(email, password)
                    self.config.save()
                except ValueError as e:
                    self.log("login", "Could not authenticate with {0} as '{1}':"
                             .format(self.config.domain, email))
                    self.log("login", str(e))
                    self.log("debug", "using the password {0}".format(password))
                    return False
            return True
        except KeyboardInterrupt:
            return False

    @error_reporter
    def default_report_handler(self, error):
        if self.mastodon and "admin" in self.config:
            self.mastodon.status_post(("@{} ERROR REPORT from {}:\n{}"
                                       .format(self.config.admin, self.name, error))[:500],
                                      visibility="direct")

    def report_error(self, error, location=None):
        """Report an error that occurred during bot operations. The default
        handler tries to DM the bot admin, if one is set, but more handlers can
        be added by using the @error_reporter decorator."""
        if location == None: location = inspect.stack()[1][3]
        self.log(location, error)
        for f in self.report_funcs:
            f(error)

    def on_notification(self, notif):
        self.log("debug", "Got a {} from {} at {}".format(notif["type"], notif["account"]["username"], notif["created_at"]))
        if (notif["type"] == "mention"):
            for f in self.reply_funcs:
                try:
                    f(notif["status"], notif["account"])
                except Exception as e:
                    error = "Fatal exception: {}\n{}".format(repr(e), traceback.format_exc())
                    self.report_error(error, f.__name__)

    def on_close(self):
        # Attempt to re-open the connection, since this is usually the result of
        # the server just dropping the connection for one reason or another.
        self.log(None, "Dropped streaming connection to {}".format(self.config.domain))
        self.stream.close()
        old_timeout = self.mastodon.request_timeout
        # Don't wait forever for the instance to respond to the /api/v1/instance endpoint inside Mastodon.py
        #self.mastodon.request_timeout = (10, 10)
        print("thing1")
        try:
            self.mastodon.instance()
        except mastodon.Mastodon.MastodonNetworkError as e:
            self.log("Instance appears to have gone down")

        print("thing2")
        wait = 10
        while(True):
            try:
                self.log(None, "Attempting to reinitialize in {}s...".format(wait))
                time.sleep(wait)
                self.stream = self.mastodon.stream_user(self, async=True)
                # Call the instance API first, so that we don't get stuck in stream_user
                #  (timeout doesn't work there for some reason)
                self.mastodon.instance()

                # If we get here without erroring, success
                self.mastodon.request_timeout = old_timeout
                self.log(None, "Successfully reinitialized streaming connection.")
                break
            except mastodon.Mastodon.MastodonNetworkError:
                self.log(None, "Timed out")
                wait *= 2
                continue

    def get_reply_visibility(self, status_dict):
        """Given a status dict, return the visibility that should be used.
        This behaves like Mastodon does by default.
        """
        # Visibility rankings (higher is more limited)
        visibility = ("public", "unlisted", "private", "direct")

        default_visibility = visibility.index(self.default_visibility)
        status_visibility = visibility.index(status_dict["visibility"])

        return visibility[max(default_visibility, status_visibility)]

    # defaults, should be replaced by concrete bots with actual implementations
    # (if necessary, anyway)
    def init(self):
        pass
    def start(self):
        pass
    def stop(self):
        pass
예제 #6
0
class Ivory():
    """
    The main Ivory class, which programmatically handles reports pulled from
    the Mastodon API.
    """
    def __init__(self, raw_config):
        """
        Runs Ivory.
        """
        # **Validate the configuration**
        config = IvoryConfig(raw_config)

        # **Set up logger**
        self._logger = logging.getLogger(__name__)

        self._logger.info("Ivory version %s starting", constants.VERSION)

        # **Load Judge and Rules**
        self._logger.info("parsing rules")
        if 'reports' in config:
            self.report_judge = ReportJudge(config['reports'].get("rules"))
        else:
            self._logger.debug("no report rules detected")
            self.report_judge = None
        if 'pendingAccounts' in config:
            self.pending_account_judge = PendingAccountJudge(
                config['pendingAccounts'].get("rules"))
        else:
            self._logger.debug("no pending account rules detected")
            self.pending_account_judge = None

        # **Initialize and verify API connectivity**
        self._api = Mastodon(access_token=config['token'],
                             api_base_url=config['instanceURL'])
        self._logger.debug("mastodon API wrapper initialized")
        # 2.9.1 required for moderation API
        if not self._api.verify_minimum_version("2.9.1"):
            self._logger.error(
                "This instance is not updated to 2.9.1 - this version is required for the Moderation API %s",
                self._api.users_moderated)
            exit(1)
        self._logger.debug("minimum version verified; should be ok")
        # grab some info which could be helpful here
        self.instance = self._api.instance()
        self.user = self._api.account_verify_credentials()
        # log a bunch of shit
        self._logger.info("logged into %s as %s", self.instance['uri'],
                          self.user['username'])
        self._logger.debug("instance info: %s", self.instance)
        self._logger.debug("user info: %s", self.user)

        # **Set some variables from config**
        if 'waitTime' not in config:
            self._logger.info(
                "no waittime specified, defaulting to %d seconds",
                constants.DEFAULT_WAIT_TIME)
        self.wait_time = config.get("waitTime", constants.DEFAULT_WAIT_TIME)
        self.dry_run = config.get('dryRun', False)

    def handle_unresolved_reports(self):
        """
        Handles all unresolved reports.
        """
        reports = self._api.admin_reports()
        for report in reports:
            self.handle_report(report)

    def handle_report(self, report: dict):
        """
        Handles a single report.
        """
        self._logger.info("handling report #%d", report['id'])
        (punishment, rules_broken) = self.report_judge.make_judgement(report)
        if rules_broken:
            self._logger.info("report breaks these rules: %s", rules_broken)
        if punishment is not None:
            self._logger.info("handling report with punishment %s", punishment)
            self.punish(report['target_account']['id'], punishment,
                        report['id'])

    def handle_pending_accounts(self):
        """
        Handle all accounts in the pending account queue.
        """
        accounts = self._api.admin_accounts(status="pending")
        for account in accounts:
            self.handle_pending_account(account)

    def handle_pending_account(self, account: dict):
        """
        Handle a single pending account.
        """
        self._logger.info("handling pending user %s", account['username'])
        (punishment,
         rules_broken) = self.pending_account_judge.make_judgement(account)
        if rules_broken:
            self._logger.info("pending account breaks these rules: %s",
                              rules_broken)
        if punishment is not None:
            self._logger.info("handling report with punishment %s", punishment)
            self._logger.debug("punishment cfg: %s", punishment.config)
            self.punish(account['id'], punishment)

    def punish(self, account_id, punishment: Punishment, report_id=None):
        if self.dry_run:
            self._logger.info("ignoring punishment; in dry mode")
            return
        maxtries = 3
        tries = 0
        while True:
            try:
                if punishment.type == constants.PUNISH_REJECT:
                    self._api.admin_account_reject(account_id)
                elif punishment.type == constants.PUNISH_WARN:
                    self._api.admin_account_moderate(
                        account_id,
                        None,
                        report_id,
                        text=punishment.config.get('message'))
                elif punishment.type == constants.PUNISH_DISABLE:
                    self._api.admin_account_moderate(
                        account_id,
                        "disable",
                        report_id,
                        text=punishment.config.get('message'))
                elif punishment.type == constants.PUNISH_SILENCE:
                    self._api.admin_account_moderate(
                        account_id,
                        "silence",
                        report_id,
                        text=punishment.config.get('message'))
                elif punishment.type == constants.PUNISH_SUSPEND:
                    self._api.admin_account_moderate(
                        account_id,
                        "suspend",
                        report_id,
                        text=punishment.config.get('message'))
                else:
                    # whoops
                    raise NotImplementedError()
                break
            except MastodonGatewayTimeoutError as err:
                self._logger.warn(
                    "gateway timed out. ignoring for now, if that didn't do it we'll get it next pass..."
                )
                break

    def run(self):
        self._logger.info("starting moderation pass")
        try:
            if self.report_judge:
                self.handle_unresolved_reports()
            if self.pending_account_judge:
                self.handle_pending_accounts()
            self._logger.info("moderation pass complete")
        except MastodonError:
            self._logger.exception(
                "enountered an API error. waiting %d seconds to try again",
                self.wait_time)

    def watch(self):
        """
        Runs handle_unresolved_reports() on a loop, with a delay specified in
        the "waittime" field of the config.
        """
        while True:
            starttime = time.time()
            self.run()
            time_to_wait = self.wait_time - (time.time() - starttime)
            if time_to_wait > 0:
                self._logger.debug("waiting for %.4f seconds", time_to_wait)
                time.sleep(time_to_wait)
            else:
                self._logger.warn(
                    "moderation pass took longer than waitTime - this will cause significant drift. you may want to increase waitTime"
                )
예제 #7
0
            tl_text += " (Compare " + ", ".join(attachments) + ")"
        tl_text += figures_text +"\n"
        
    return(tl_text)

api = Mastodon(
    client_id = sys.argv[2],
    access_token = sys.argv[3],
    api_base_url = sys.argv[1]
)

while(True):
    with open('samplepaper.tex', 'r') as template_file:
        latex_base = template_file.read()

    instance_info = api.instance()
    user_info = api.account_verify_credentials()

    home_tl = convert_tl(api.timeline_home())
    local_tl = convert_tl(api.timeline_local())
    fed_tl = convert_tl(api.timeline_public())

    latex_base = latex_base.replace("INSTANCENAME", instance_info.title)
    latex_base = latex_base.replace("INSTANCEURL", "https://" + instance_info.uri)
    latex_base = latex_base.replace("INSTANCEDESC", cleanhtml(instance_info.description))
    latex_base = latex_base.replace("USERNAME", user_info.acct)
    latex_base = latex_base.replace("USERURL", user_info.url)

    latex_base = latex_base.replace("HOMETL", home_tl)
    latex_base = latex_base.replace("LOCALTL", local_tl)
    latex_base = latex_base.replace("FEDTL", fed_tl)