import argparse import configparser import os import sys from timeit import default_timer as timer from sqlalchemy import create_engine, exc from sqlalchemy.orm import sessionmaker import quarg.actions as actions import quarg.database.filters as filters import quarg.quassel.formatter as formatter from quarg.database.tables import Backlog, Buffer, Network, QuasselUser, Sender from quarg.utils import errx # pylint: disable=line-too-long cli = argparse.ArgumentParser() cli.add_argument('keyword', nargs='*', help='match messages containing this keyword') cli.add_argument('-d', action='store_true', dest='debug', help='print debug and SQL query information') cli.add_argument('-e', action='store_true', dest='expr', help='interpret keywords as LIKE expression') cli.add_argument('-l', dest='limit', metavar='NUM', type=int, help='limit the number of matches') cli.add_argument('-o', action=actions.ParseOrder, dest='order', metavar='asc|desc', help='sort matches in this order') matchers = cli.add_argument_group('matching message context') matchers.add_argument('-b', action='append', dest='buffer', help='match this buffer') matchers.add_argument('-B', action=actions.ParseBufferType, dest='buftype', help='match buffers of this type') matchers.add_argument('-n', action='append', dest='nick', help='match this nickname') matchers.add_argument('-m', action='append', dest='prefix', help='match nicknames with this channel membership prefix') matchers.add_argument('-N', action='append', dest='network', help='match this network') matchers.add_argument('-Q', action='append', dest='user', help='match this quassel user') matchers.add_argument('-t', action=actions.ParseMessageType, dest='msgtype', help='match this message type') matchers.add_argument('-f', action=actions.ParseMessageFlag, dest='msgflag', help='match this message flag') date_matchers = cli.add_argument_group('matching message timestamps') date_matchers.add_argument('--after', action=actions.ParseDate, metavar='DATE', help='sent after this date') date_matchers.add_argument('--before', action=actions.ParseDate, metavar='DATE', help='sent before this date') date_matchers.add_argument('--around', action=actions.ParseAround, metavar='DATE[/RANGE]', help='sent RANGE hours/minutes before or after this date') joined_group = matchers.add_mutually_exclusive_group() joined_group.add_argument('--joined', default=None, action='store_true', dest='joined', help='match buffers which are currently joined') joined_group.add_argument('--no-joined', default=None, action='store_false', dest='joined', help='match buffers which are not currently joined') # pylint: enable=line-too-long Session = sessionmaker() def get_config(): xdg_config_home = os.path.expanduser('~/.config/') if 'XDG_CONFIG_HOME' in os.environ: xdg_config_home = os.environ['XDG_CONFIG_HOME'] path = os.path.join(xdg_config_home, 'quarg', 'config') config = configparser.ConfigParser() config.read(path) return config def check_args(args): if args.around is not None: if args.before is not None or args.after is not None: errx('--around cannot be used with --after or --before') def collect_predicates(args): funs = { 'keyword': filters.msg_like if args.expr else filters.msg_contains, 'buffer': filters.buffer, 'nick': filters.nick, 'after': filters.time_after, 'before': filters.time_before, 'around': filters.time_around, 'user': filters.user, 'network': filters.network, 'msgflag': filters.msgflag, 'msgtype': filters.msgtype, 'buftype': filters.buftype, 'prefix': filters.prefix, 'joined': filters.joined, } for key, value in vars(args).items(): # ignore unset or empty arguments # Note: 'if not value' does not work here because 'joined' can be falsy # but still needs to be passed to filters.joined if value is None or value == []: continue # ignore arguments that do not map to predicates if key not in funs: continue if args.debug: print(f'{key}: {value}', file=sys.stderr) fun = funs[key] if isinstance(value, list): yield filters.any_filter(fun, value) else: yield fun(value) def prepare_query(session, predicates, args): query = session.query(Backlog).join(Sender).join(Buffer).join(Network).join(QuasselUser) for predicate in predicates: query = query.filter(predicate) order = Backlog.time.asc() if args.order == 'desc': order = Backlog.time.desc() query = query.order_by(order) if args.limit: query = query.limit(args.limit) return query def time_query(query): start = timer() rows = query.all() end = timer() return rows, end - start def main(): args = cli.parse_intermixed_args() check_args(args) config = get_config() if not config.has_option('Database', 'url'): errx('No database URL set in config file.') engine = create_engine(config.get('Database', 'url'), echo=args.debug) session = Session(bind=engine) predicates = list(collect_predicates(args)) if not predicates: errx('Nothing to match.') try: query = prepare_query(session, predicates, args) rows, time = time_query(query) for row in rows: print(formatter.format_from(row)) except exc.SQLAlchemyError as err: errx(err) except (KeyboardInterrupt, BrokenPipeError): session.rollback() session.close() sys.exit(1) print(f'Query returned {len(rows)} lines in {time:.4f} seconds.', file=sys.stderr) if __name__ == "__main__": main()