#!/usr/bin/python
#
# NOTE: The first time this script contacts launchpad, a browser will be
# started asking you to log in to launchpad and give permissions to this script.
#
# This script should be functionally equivalent to b-tag.sh
# To use from within a mail client to add a tag to a bug currently being
# read by your client, do:
#	| b-tool -a TAG
#
# You can test your changes by using the staging server like so:
#	| b-tool --debug -a TAG
#
# You can also mix and match tags to remove and pass bugnumbers on the
# command line, e.g.
#	b-tool -a verification-needed -r verification-failed 123456 234567
#
# A comment can be added to all the bugs if given with the --comment
# argument, e.g.
#	b-tool -a regression-potential 111111 121212 --comment "We hates regressssssions."
#
# You can also use this tool to mark a bunch of bugs as duplicates of
# another bug with the --duplicate=DUPE argument; e.g. to mark bugs
# 111111 and 121212 as duplicates of bug 123456, do:
#
#	b-tool --duplicate 123456 111111 121212 --comment "We hates duplicatessss."
#
# b-tool can also be used to subscribe yourself and others (via their
# launchpad id) to bugs; e.g.:
#
#	b-tool --subscribe --subscribe-id sru-verification  111111 121212 --comment "We hates subsssscriptionss."
#
# will subscribe yourself (via --subscribe) and the sru-verification
# team to bugs 111111 and 121212
#
# you can also unsubscribe yourself from bugs with --unsubscribe, but
# due to https://bugs.launchpad.net/malone/+bug/281028 you cannot
# unsubscribe others. e.g.
#
#	b-tool --unsubscribe 111111 121212 --comment "We really hates subsssscriptionss."
#
# will unsubscribe you from bugs 111111 and 121212 (if you are
# subscribed).
#
# Mutt example of tagging bugs
# ----------------------------
# To map ',h' to marking the currently read bugmail with the hw-specific
# tag, one would add the following to their .muttrc:
#
#   macro pager ",h" "<pipe-message> /path/to/b-tool -a hw-specific"
#
# To map ,h to add the hw-specific tag to all of the mutt-tagged emails
# in mutt's index view, add:
#
#   macro index ",h" "<tag-prefix><pipe-message> /path/to/b-tool -a hw-specific\r<tag-prefix><tag-message>"
#
#
# credentials and cached launchpadlib objects are saved under
# $HOME/.launchpadlib.
#
# Written by Steve Beattie <sbeattie@ubuntu.com>
# Copyright 2008, 2009 Canonical Ltd.
# Licensed under the GNU General Public License, version 3.

import re
import os
import sys
from optparse import OptionParser
from launchpadlib.launchpad import Launchpad, STAGING_SERVICE_ROOT, EDGE_SERVICE_ROOT
from launchpadlib.credentials import Credentials
from mailbox import PortableUnixMailbox
from email.errors import MessageParseError
from email.utils import parseaddr
import tempfile

client_name = "b-tool"

def get_creds(creds_dir, config):
    if (config.use_staging):
	root = STAGING_SERVICE_ROOT
	server = "staging"
    elif (not config.use_staging):
	root = EDGE_SERVICE_ROOT
	server = "edge"
    else:
        assert "config.use_staging is set to something odd: " + str(config.use_staging)

    cachedir = creds_dir + "/cache"
    if not os.path.exists(cachedir):
	os.makedirs(cachedir,0700)

    cred_name = creds_dir + "/" + client_name + "-" + server + ".cred"

    if os.path.exists(cred_name) and not config.reset_creds:
        credentials = Credentials()
	credentials.load(open(cred_name))
	launchpad = Launchpad(credentials, root, cachedir)
    else:
	launchpad = Launchpad.get_token_and_login(client_name, root, cachedir)
    	launchpad.credentials.save(os.fdopen(os.open(cred_name, os.O_WRONLY | os.O_CREAT, 0600), "w"))

    return launchpad

# returns bug number list
def parse_mail(fd):
    reg = re.compile('(\d+)@bugs.launchpad.net', re.M | re.I)
    bset = set()

    for msg in PortableUnixMailbox(fd):
	name, email = parseaddr(msg.get('reply-to'))
	val = reg.match(email)
	if (val):
	    #print "Found bug " + val.group(1)
	    bset.add(val.group(1))

    return list(bset)

def add_comment(bug, message):
    subject = bug.title
    bug.newMessage(subject=subject, content=message)

def edit_tags(bug, tags_add, tags_remove):
    new_tags = []
    for tag in bug.tags:
	if not tag in tags_remove:
	    new_tags.append(tag)
	
    new_tags.extend(tags_add)

    bug.tags = new_tags
    print_status(bug, tags_add, tags_remove)
    bug.lp_save()

def print_status(bug, tags_add, tags_remove):
    s = "bug " + str(bug.id) + ": updating tags with"
    for tag in tags_add:
	s += " +" + tag
    for tag in tags_remove:
	s += " -" + tag
    print s

def mark_duplicate(orig, dupe):
    dupe.duplicate_of = orig
    print "bug " + str(dupe.id) + ": marking as duplicate of bug " + str(orig.id)
    dupe.lp_save()

def subscribe(bug, lp_id):
    print "bug " + str(bug.id) + ": subscribing " + lp_id.display_name
    bug.subscribe(person=lp_id)

def unsubscribe(bug, lp_id):
    # this currently does not work due to
    # https://bugs.launchpad.net/malone/+bug/281028
    print "bug " + str(bug.id) + ": unsubscribing " + lp_id.display_name
    bug.unsubscribe(person=lp_id)

def slurp_input(fd):
    # because python's mailbox parser is lame and needs an actual
    # seekable file, so we create one. Sigh.
    file = tempfile.TemporaryFile(prefix='b-tag.py-mbox')
    file.writelines(fd.readlines())
    return file

def main():

    parser = OptionParser()
    parser.add_option('-a', '--add-tag', action='append', dest='tags_add', default=[], help='specify tag to add to the bug(s)')
    parser.add_option('-c', '--comment', action='store', dest='comment', help='add the comment COMMENT to the bugreport(s)')
    parser.add_option('-d', '--debug', action='store_true', dest='use_staging', default=False, help='debug/use the staging server for testing')
    parser.add_option('-D', '--dupe', '--duplicate', action='store', dest='dupe', help='mark bug(s) as duplicates of DUPE')
    parser.add_option('-r', '--remove-tag', action='append', dest='tags_remove', default=[], help='specify tag to remove from the bug(s)')
    parser.add_option('-R', '--reset-credentials', action='store_true', dest='reset_creds', default=False, help='reset launchpad credentials first')
    parser.add_option('-s', '--subscribe', action='store_true', dest='subscribe_me', default=False, help='subscribe to the bug(s)')
    parser.add_option('-S', '--subscribe-id', action='append', dest='sub_id', default=[], help='subscribe SUB_ID to the bug(s)')
    parser.add_option('--state', action='store_true', dest='state', default=False, help='display state of bug tasks')
    parser.add_option('-u', '--unsubscribe', action='store_true', dest='unsubscribe_me', default=False, help='unsubscribe to the bug(s)')
    parser.add_option('--edge', action='store_false', dest='use_staging', help='use the edge server for LIVE UPDATES')
    (options, args) = parser.parse_args()

    if (len(args) == 0):
	args.extend(parse_mail(slurp_input(sys.stdin)))

    try:
	lplibdir = os.environ['HOME'] + "/.launchpadlib"
    except (KeyError), e:
	print "$HOME is not set, aborting"
	exit(1)

    launchpad = get_creds(lplibdir, options)

    try:
	if (options.dupe):
	    orig = launchpad.bugs[int(options.dupe)]
    except (ValueError, KeyError), e:
	print "Duplicate arg \"" + options.dupe + "\" is not a valid bugnumber, exiting."
	print str(e)
	os.exit(1)

    if (options.subscribe_me or options.unsubscribe_me):
	lp_me = launchpad.me

    sub_ids = []
    if len(options.sub_id) > 0:
	for id in options.sub_id:
	    try:
		sub_ids.append(launchpad.people[id])
	    except (ValueError, KeyError), e:
		print "id \"" + id + "\" is not a valid launchpad id, skipping."
	        print str(e)

    for arg in args:
        try:
	    n = int(arg)
    	    bug = launchpad.bugs[n]

	    if (len(options.tags_add) + len(options.tags_remove) > 0):
	        edit_tags(bug, options.tags_add, options.tags_remove)

	    if (options.dupe):
	        mark_duplicate(orig, bug)

	    if (options.subscribe_me):
		subscribe(bug, lp_me)

            if len(sub_ids) > 0:
		for lp_id in sub_ids:
		    subscribe(bug, lp_id)

	    if (options.unsubscribe_me):
	        # use this when bug 281028 is fixed
		#unsubscribe(bug, lp_me)
    		print "bug " + str(bug.id) + ": unsubscribing " + lp_me.display_name
		bug.unsubscribe()

	    if (options.comment):
	        add_comment(bug, options.comment)

            if (options.state):
                for task in bug.bug_tasks:
                    milestone = 'none'
                    if task.milestone:
                        milestone = task.milestone.name
                    # so it looks like email headers from Launchpad
                    print "sourcepackage=%s; milestone=%s; status=%s; importance=%s" % (task.bug_target_display_name, milestone, task.status, task.importance)

	except (ValueError, KeyError), e:
	    print "Argument \"" + arg + "\" is not a valid bugnumber, skipping."
	    print str(e)

if __name__ == "__main__":
    main()
