# Path: cardinal/decorators.py
# def command(triggers):
#     if isinstance(triggers, str):
#         triggers = [triggers]
# 
#     if not isinstance(triggers, list):
#         raise TypeError("Command must be a trigger string or list of triggers")
# 
#     def wrap(f):
#         f.commands = triggers
#         return f
# 
#     return wrap
# 
# def help(lines):
#     # For backwards compatibility
#     if isinstance(lines, str):
#         lines = [lines]
# 
#     if not isinstance(lines, list):
#         raise TypeError("Help must be a help string or list of help strings")
# 
#     def wrap(f):
#         # Create help list or prepend to it
#         if not hasattr(f, 'help'):
#             f.help = lines
#         else:
#             f.help = lines + f.help
# 
#         return f
# 
#     return wrap
# 
# def event(triggers):
#     if isinstance(triggers, str):
#         triggers = [triggers]
# 
#     if not isinstance(triggers, list):
#         raise TypeError("Event must be a trigger string or list of triggers")
# 
#     def wrap(f):
#         f.events = triggers
#         return f
# 
#     return wrap
# 
# Path: cardinal/util.py
# def is_action(message):
#     """Checks if a message is a /me message."""
#     return message.startswith("\x01ACTION")
# 
# def parse_action(nick, message):
#     """Parses a /me message like an IRC client might.
# 
#     e.g. "/me dances." -> "* Cardinal dances."
#     """
#     if not is_action(message):
#         raise ValueError("This message is not an ACTION message")
# 
#     message = message[len("\x01ACTION "):]
#     if message and message[-1] == "\x01":
#         message = message[:-1]
# 
#     return "* {}{}".format(
#         nick,
#         " " + message if message else "",
#     )
# 
# def strip_formatting(line):
#     """Removes mIRC control code formatting"""
#     return re.sub(r"(?:\x03\d\d?,\d\d?|\x03\d\d?|[\x01-\x1f])", "", line)


from datetime import datetime, timezone
from cardinal.decorators import command, help, event
from cardinal.util import (
    is_action,
    parse_action,
    strip_formatting,
)
            config = {}

        self.cardinal = cardinal
        self.ignored_channels = config.get('ignored_channels', [])

        self.db = cardinal.get_db('seen')
        with self.db() as db:
            if 'users' not in db:
                db['users'] = {}

            if 'tells' not in db:
                db['tells'] = {}

            # Fix case for old databases
            users = dict()
            for k, v in db['users'].items():
                users[k.lower()] = v
            db['users'] = users

    def update_user(self, nick, action, params):
        if not isinstance(params, list):
            raise TypeError("params must be a list")

        with self.db() as db:
            db['users'][nick.lower()] = {
                'timestamp': datetime.now(tz=timezone.utc).timestamp(),
                'action': action,
                'params': params,
            }


# Path: cardinal/decorators.py
# def command(triggers):
#     if isinstance(triggers, str):
#         triggers = [triggers]
# 
#     if not isinstance(triggers, list):
#         raise TypeError("Command must be a trigger string or list of triggers")
# 
#     def wrap(f):
#         f.commands = triggers
#         return f
# 
#     return wrap
# 
# def help(lines):
#     # For backwards compatibility
#     if isinstance(lines, str):
#         lines = [lines]
# 
#     if not isinstance(lines, list):
#         raise TypeError("Help must be a help string or list of help strings")
# 
#     def wrap(f):
#         # Create help list or prepend to it
#         if not hasattr(f, 'help'):
#             f.help = lines
#         else:
#             f.help = lines + f.help
# 
#         return f
# 
#     return wrap
# 
# def event(triggers):
#     if isinstance(triggers, str):
#         triggers = [triggers]
# 
#     if not isinstance(triggers, list):
#         raise TypeError("Event must be a trigger string or list of triggers")
# 
#     def wrap(f):
#         f.events = triggers
#         return f
# 
#     return wrap
# 
# Path: cardinal/util.py
# def is_action(message):
#     """Checks if a message is a /me message."""
#     return message.startswith("\x01ACTION")
# 
# def parse_action(nick, message):
#     """Parses a /me message like an IRC client might.
# 
#     e.g. "/me dances." -> "* Cardinal dances."
#     """
#     if not is_action(message):
#         raise ValueError("This message is not an ACTION message")
# 
#     message = message[len("\x01ACTION "):]
#     if message and message[-1] == "\x01":
#         message = message[:-1]
# 
#     return "* {}{}".format(
#         nick,
#         " " + message if message else "",
#     )
# 
# def strip_formatting(line):
#     """Removes mIRC control code formatting"""
#     return re.sub(r"(?:\x03\d\d?,\d\d?|\x03\d\d?|[\x01-\x1f])", "", line)


# Path: plugins/seen/plugin.py
from datetime import datetime, timezone
from cardinal.decorators import command, help, event
from cardinal.util import (
    is_action,
    parse_action,
    strip_formatting,
)

            config = {}

        self.cardinal = cardinal
        self.ignored_channels = config.get('ignored_channels', [])

        self.db = cardinal.get_db('seen')
        with self.db() as db:
            if 'users' not in db:
                db['users'] = {}

            if 'tells' not in db:
                db['tells'] = {}

            # Fix case for old databases
            users = dict()
            for k, v in db['users'].items():
                users[k.lower()] = v
            db['users'] = users

    def update_user(self, nick, action, params):
        if not isinstance(params, list):
            raise TypeError("params must be a list")

        with self.db() as db:
            db['users'][nick.lower()] = {
                'timestamp': datetime.now(tz=timezone.utc).timestamp(),
                'action': action,
                'params': params,
            }


    @event('irc.privmsg')