aboutsummaryrefslogblamecommitdiffstatshomepage
path: root/quarg/main.py
blob: 5ba6e3d13cf5cb92b0aba392defaf2cb79ee0b97 (plain) (tree)
1
2
3
4
5
6
7
8
9



                   

                                         
                                         

                                       
                               
                                        
                                           
                                                                               
                            
 

                               


                                                                                                       
                                                                                                 
                                                                                                                      



                                                                                                              
                                                                                                                       






                                                                                                              
                                                                                                                                                     



                                                                                                                                                















                                                           




                                                                    

                             
                                                                             

                                  

                                       





                                       
                                  



                                         


















                                                                               
 
                                             




                                                                                            







                                       
 
                
 


                      
                 
                            

           
                                      

                    




                                                   







                                                                          


                                                        
 



                                             
                                                


                          




                                                                                      
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()