def get_default_list(client, board):
    # XXX also insert in bridge?
    default_list = get_none(client.wekan.lists, {
        "title": "No roadmap [UNMILESTONE]",
        "boardId": board["_id"]
    })

    if default_list:
        print "'No roadmap' list already exists, return it"
        return default_list["_id"]

    all_lists = list(client.wekan.lists.find({"boardId": board["_id"]}))
    if all_lists:
        sort = 1 + max([x.get("sort", 0) for x in all_lists])
    else:
        sort = 0

    print "'No roadmap' didn't exist, create it"
    return client.wekan.lists.insert({
        "_id": generate_id(),
        "title": "No roadmap [UNMILESTONE]",
        "boardId": board["_id"],
        "archived": False,
        "createdAt": datetime.now(),
        "sort": sort
    })
def get_user(client, author):
    bridge_user = get_none(client.wekan.bridge_for_users,
                           {"username": author["login"]})

    if bridge_user:
        print "Found bridge user, return matching user (%s)" % bridge_user[
            "wekan_id"]
        return get_by_id(client.wekan.users, bridge_user["wekan_id"])["_id"]

    user = get_none(client.wekan.users, {"username": author["login"]})

    if user is None:
        # TODO I need to grab the real user to fill the data, I only have the actor edge here
        user = client.wekan.users.insert({
            "_id": generate_id(),
            "createdAt": datetime.now(),
            "services": {
                "password": {},
                "email": {},
                "resume": {}
            },
            "username": author["login"],
            "emails": [],
            "isAdmin": False,
            "profile": {
                # "fullname" : "Testeu",
                # TODO copy file
                # "avatarUrl" : "/cfs/files/avatars/vvwa5nMFBRCbMc6p6/2-punaise-des-bois-pentatomes.jpg"
            }
        })
    else:
        user = user["_id"]

    bridge_user = client.wekan.bridge_for_users.insert({
        "github_id":
        author["login"],
        "wekan_id":
        user
    })

    return user
def wekan(secret):
    print request
    print request.json

    # sekuritay
    local_secret = open("./wekan_webhook_secret", "r").read().strip()

    if local_secret != secret.strip():
        abort(403)

    # TODO check that request is json and correct or some stuff like that

    # card create -> we don't care about it

    # if card not linked to github, we don't care about it

    # card archival
    # * does that mean that we close the PR?
    # * do we have unarchival event?

    # comment creation
    # * not yet supported

    # card move -> interesting part
    # TODO: mark "[MILESTONE]" or something like that in columns titles
    # * if card move in another milestone than its one -> change of milestone
    # * if card is moved out of milestone keep its milestone

    # we don't have card modification like title or description?

    # do we have milestone modifications events?
    # apparently no :(

    client = MongoClient()

    if request.json["description"] == "act-moveCard":
        card_id = request.json["cardId"]
        card = get_by_id(client.wekan.cards, card_id)

        pr_bridge = get_none(client.wekan.bridge_for_prs,
                             {"wekan_id": card_id})

        print "bridge:", pr_bridge
        print "card:", card

        if pr_bridge is None:
            print "unhandled card", card_id
            return "unhandled card"

        project = pr_bridge["github_project"]

        list_id = request.json["listId"]

        list_ = get_none(client.wekan.bridge_for_milestones, {
            "wekan_id": list_id,
            "github_project": project
        })

        # list itself can't be none
        # list can be: know (a milestone), unknow
        # card was in a milestone, wasn't [back to its milestone or in another milestone]

        github_pr = requests.get(
            "https://api.github.com/repos/yunohost/%s/pulls/%s" %
            (project, pr_bridge["github_id"])).json()

        github_milestone_id = github_pr["milestone"]["number"] if github_pr[
            "milestone"] is not None else None

        if list_ is None:
            # try to detect if there is a list that is a milestone with
            # the same name but that isn't in that project, if so, create the
            # milestone in the project where it's missing

            list_bridge = list(
                client.wekan.bridge_for_milestones.find({"wekan_id": list_id}))

            if not list_bridge:
                print "new list is not known as a milestone, skip"
                return "ok"

            list_bridge = list_bridge[0]
            print "list is linked to milestone of other projects"
            print "-> create a milestone with the same name in '%s' project" % project

            # create the milestone on github on the project that hasn't have it
            list_ = get_by_id(client.wekan.lists, list_id)
            github_milestone = requests.post(
                "https://api.github.com/repos/yunohost/%s/milestones" %
                project,
                json={
                    "title": list_["title"].replace(" [MILESTONE]",
                                                    "").strip()
                },
                headers={"Authorization": "bearer %s" % token})
            print github_milestone
            print github_milestone.json()

            bridge_milestone_id = client.wekan.bridge_for_milestones.insert({
                "github_id":
                github_milestone.json()["number"],
                "github_project":
                project,
                "wekan_id":
                list_id
            })

            list_ = get_by_id(client.wekan.bridge_for_milestones,
                              bridge_milestone_id)

        # check if target column milestone number != github_milestone_id
        # if so, change it
        # else return
        if list_["github_id"] != github_milestone_id:
            print "online github PR is different than the targeted list, change it"
            print requests.patch(
                "https://api.github.com/repos/yunohost/%s/issues/%s" %
                (project, pr_bridge["github_id"]),
                json={"milestone": list_["github_id"]},
                headers={"Authorization": "bearer %s" % token})

            query = open("./query-one.graphql", "r").read()
            pr = requests.post("https://api.github.com/graphql",
                               headers={
                                   "Authorization": "bearer %s" % token
                               },
                               json={
                                   "query":
                                   query % (project, pr_bridge["github_id"])
                               }).json()

            import_pr(client, project, pr["data"]["repository"]["pullRequest"])
        else:
            print "online github PR is the same than the targeted list, don't do anything"
            return "ok"

    return "ok"
def github():
    # print request
    # print request.json

    # github_hook_secret = open("./secret_for_webhook", "r").read().strip()

    # > HMAC hex digest of the payload, using the hook's secret as the key (if
    # > configured)
    # if request.headers.get("X-Hub-Signature").strip() != github_hook_secret:
    # TODO real exception
    # raise 400

    # stolen and adapted from here
    # https://github.com/carlos-jenkins/python-github-webhooks/blob/d485b31c0291d06b5153198bc1de685d50731536/webhooks.py#L72-L93
    secret = open("./github_webhook_secret", "r").read().strip()

    # Only SHA1 is supported
    header_signature = request.headers.get('X-Hub-Signature')
    if header_signature is None:
        print "no header X-Hub-Signature"
        abort(403)

    sha_name, signature = header_signature.split('=')
    if sha_name != 'sha1':
        print "signing algo isn't sha1, it's '%s'" % sha_name
        abort(501)

    # HMAC requires the key to be bytes, but data is string
    mac = hmac.new(str(secret), msg=request.data, digestmod=hashlib.sha1)

    if not hmac.compare_digest(str(mac.hexdigest()), str(signature)):
        abort(403)

    hook_type = request.headers.get("X-Github-Event")

    print "Hook type:", hook_type

    if hook_type == "pull_request":
        token = open("graphql_token", "r").read().strip()
        query = open("./query-one.graphql", "r").read()

        project = request.json["repository"]["name"]
        number = request.json["pull_request"]["number"]

        pr = requests.post("https://api.github.com/graphql",
                           headers={
                               "Authorization": "bearer %s" % token
                           },
                           json={
                               "query": query % (project, number)
                           }).json()

        print "reimporting pr %s#%s" % (project, number)
        import_pr(client, project, pr["data"]["repository"]["pullRequest"])

    elif hook_type == "milestone":
        # actions: created, closed, opened, edited, deleted

        board = get_board(client)
        project = request.json["repository"]["name"]

        if request.json["action"] == "created":
            # I need to import it here

            # this is badly name but will create list (column) on the fly
            get_list_for_milestone(client, board, project,
                                   request.json["milestone"])

        elif request.json["action"] in ("closed", "deleted"):
            print "Milestone %s#%s as been closed on github" % (
                project, request.json["milestone"]["number"])
            bridge_milestone = get(
                client.wekan.bridge_for_milestones, {
                    "github_id": request.json["milestone"]["number"],
                    "github_project": project
                })

            # if all milestone pointing to this list are closed, archive to
            # column assuming the milestone exist
            print "checking if all other PR are closed"
            all_closed = True
            # get all milestone pointing to this list
            # this sucks because I need to contact github api
            for other_miletone in client.wekan.bridge_for_milestones.find(
                {"wekan_id": bridge_milestone["wekan_id"]}):
                # GET /repos/:owner/:repo/milestones/:number
                other_project = other_miletone["github_project"]
                print "https://api.github.com/repos/yunohost/%s/milestones/%s" % (
                    other_project, other_miletone["github_id"])
                other_miletone = requests.get(
                    "https://api.github.com/repos/yunohost/%s/milestones/%s" %
                    (other_project, other_miletone["github_id"])).json()

                if other_miletone["state"] == "open":
                    print "milestone of '%s' is not closed (and maybe other), stop" % other_project
                    all_closed = False
                    break

            list_ = get_by_id(client.wekan.lists, bridge_milestone["wekan_id"])

            list_is_empty = len(
                list(
                    client.wekan.cards.find({
                        "listId": list_["_id"],
                        "archived": False
                    }))) == 0

            if all_closed and list_is_empty:
                print "all other milestone are closed, closing"
                client.wekan.lists.update({"_id": list_["_id"]},
                                          {"$set": {
                                              "archived": True
                                          }})

            if request.json["action"] == "deleted":
                client.wekan.bridge_for_milestones.remove({
                    "github_id":
                    request.json["milestone"]["number"],
                    "github_project":
                    project
                })

                # move all cards out in "no milestone" list
                print "deleting action, moving all cards "
                list_ = get_default_list(client, board)
                for card in client.wekan.cards.find({"listId": list_["_id"]}):
                    # TODO update title
                    # should I just uses the "import_pr" here?
                    client.wekan.lists.update(
                        {"_id": list_["_id"]},
                        {"$set": {
                            "listId": list_["_id"]
                        }})

        elif request.json["action"] == "opened":
            # set column state to unarchived
            # assuming the milestone exist
            bridge_milestone = get(
                client.wekan.bridge_for_milestones, {
                    "github_id": request.json["milestone"]["number"],
                    "github_project": project
                })
            list_ = get_by_id(client.wekan.lists, bridge_milestone["wekan_id"])
            client.wekan.lists.update({"_id": list_["_id"]},
                                      {"$set": {
                                          "archived": False
                                      }})

        elif request.json["action"] == "edited":
            print request.json["changes"]

            bridge_milestone = get(
                client.wekan.bridge_for_milestones, {
                    "github_id": request.json["milestone"]["number"],
                    "github_project": project
                })

            # only care about title change
            if not request.json["changes"].get("title"):
                return "ok"

            new_title = request.json["milestone"]["title"]
            new_list_title = "%s [MILESTONE]" % new_title
            list_ = get_by_id(client.wekan.lists, bridge_milestone["wekan_id"])

            # if I'm the only milestone on that column rename it
            if len(
                    list(
                        client.wekan.bridge_for_milestones.find(
                            {"wekan_id": bridge_milestone["wekan_id"]}))) == 1:
                print "I'm the only milestone on that column rename it"
                print list(
                    client.wekan.bridge_for_milestones.find(
                        {"wekan_id": bridge_milestone["wekan_id"]}))
                client.wekan.lists.update({"_id": list_["_id"]},
                                          {"$set": {
                                              "title": new_list_title
                                          }})

                # rename card: change milestone titles
                for card in client.wekan.cards.find({"listId": list_["_id"]}):
                    old_milestone_card_title = "{%s}" % request.json[
                        "changes"]["title"]["from"].lower().replace(" ", "-")
                    new_milestone_card_title = "{%s}" % request.json[
                        "milestone"]["title"].lower().replace(" ", "-")
                    title = card["title"].replace(old_milestone_card_title,
                                                  new_milestone_card_title, 1)
                    print "rename card: '%s' -> '%s'" % (card["title"], title)
                    client.wekan.cards.update({"_id": card["_id"]},
                                              {"$set": {
                                                  "title": title
                                              }})

                return "ok"

            # I'm not the only milestone pointing on that column

            target_list = get_none(client.wekan.lists,
                                   {"title": new_list_title})
            print "new_list_title:", new_list_title
            print "target list:", target_list

            board = get_board(client)

            # are they any other column with the same new title?
            if target_list:
                # if so, merge into it
                print "there is one list with the same title, merge into it"
                list_id = target_list["_id"]
            else:
                # else, create new list
                print "no other colum with new title, create a new one", request.json[
                    "milestone"]
                sort = 1 + max([
                    x.get("sort", 0)
                    for x in client.wekan.lists.find({"boardId": board["_id"]})
                ])

                print "create new list '%s'" % new_list_title
                list_id = client.wekan.lists.insert({
                    "_id": generate_id(),
                    "title": new_list_title,
                    "boardId": board["_id"],
                    "archived": False,
                    "createdAt": datetime.now(),
                    "sort": sort
                })

                client.wekan.bridge_for_milestones.update(
                    {
                        "github_project": bridge_milestone["github_project"],
                        "github_id": bridge_milestone["github_id"]
                    }, {"$set": {
                        "wekan_id": list_id
                    }})

            # move all the milestone cards into the new target list
            for bridge_pr in client.wekan.bridge_for_prs.find(
                {"github_project": project}):
                card = get_by_id(client.wekan.cards, bridge_pr["wekan_id"])

                if card["listId"] == bridge_milestone["wekan_id"]:
                    cards_in_column = list(
                        client.wekan.cards.find({
                            "boardId": board["_id"],
                            "listId": list_
                        }))
                    sort = 1 + (max(
                        [x.get("sort", 0)
                         for x in cards_in_column]) if cards_in_column else -1)

                    old_milestone_card_title = "{%s}" % request.json[
                        "changes"]["title"]["from"].lower().replace(" ", "-")
                    new_milestone_card_title = "{%s}" % request.json[
                        "milestone"]["title"].lower().replace(" ", "-")
                    title = card["title"].replace(old_milestone_card_title,
                                                  new_milestone_card_title, 1)

                    print "'%s' (%s): %s -> %s [%s]" % (
                        card["title"], card["_id"],
                        bridge_milestone["wekan_id"], list_id, sort)
                    print "rename card '%s' -> '%s'" % (card["title"], title)
                    client.wekan.cards.update({"_id": card["_id"]}, {
                        "$set": {
                            "listId": list_id,
                            "sort": sort,
                            "title": title
                        }
                    })

        else:
            print "unkown action for milestone webhook: '%s'" % request.json[
                "action"]

    elif hook_type == "label":
        # TODO
        pass

    # XXX really need to do that?
    elif hook_type == "user":
        # TODO
        pass

    else:
        print "unsupported hook type: %s" % hook_type

    return "ok"
def import_pr(client, project, pr):
    # TODO create the bord if it doesn't exist
    board = get_board(client)

    print "Working on '%s' (%s)" % (pr["title"], pr["url"])

    bridge_pr = get_none(client.wekan.bridge_for_prs, {
        "github_id": pr["number"],
        "github_project": project
    })

    # get list for milestone
    milestone = pr["milestone"]
    if milestone is None:
        print "No milestone"
        list_ = get_default_list(client, board)
        title = "%s#%s %s" % (project.upper(), pr["number"], pr["title"])
        print "selected default list (%s)" % (list_)
    else:
        print "Has milestone"
        list_ = get_list_for_milestone(client, board, project, milestone)

        # include milestone in card title
        title = "%s#%s {%s} %s" % (project.upper(), pr["number"],
                                   milestone["title"].lower().replace(
                                       " ", "-"), pr["title"])

        print "selected list '%s' (%s)" % (milestone["title"], list_)

    if pr["labels"]["edges"]:
        title += " %s" % " ".join([
            "#" + x["node"]["name"].lower().replace(" ", "-")
            for x in pr["labels"]["edges"]
        ])

    # get user for ticket
    user = get_user(client, pr["author"])
    print "selected user '%s' (%s)" % (pr["author"]["login"], user)

    # we haven't imported this PR yet
    if not bridge_pr:
        cards_in_column = list(
            client.wekan.cards.find({
                "boardId": board["_id"],
                "listId": list_
            }))
        sort = 1 + (max([x.get("sort", 0)
                         for x in cards_in_column]) if cards_in_column else -1)

        card = client.wekan.cards.insert({
            "_id": generate_id(),
            "title": title,
            "description": pr["url"],
            "members": [],
            "labelIds": [],
            "listId": list_,
            "boardId": board["_id"],
            "sort": sort,
            "archived": pr["closed"],
            "createdAt": datetime.now(),  # XXX uses card value?
            "dateLastActivity": datetime.now(),
            "userId": user
        })
        print "create card '%s' (%s)" % (pr["title"], card)

        bridge_pr = client.wekan.bridge_for_prs.insert({
            "github_id":
            pr["number"],
            "github_project":
            project,
            "wekan_id":
            card,
        })
        print "create card bridge %s -> %s" % (pr["number"], card)

    else:
        print "Card already exist, update"
        card = get_by_id(client.wekan.cards, bridge_pr["wekan_id"])

        if card["title"] != title:
            print "change title from '%s' to '%s'" % (card["title"], title)
            card["title"] = title

        if card["listId"] != list_:
            before_list = get_by_id(client.wekan.lists, card["listId"])
            after_list = get_by_id(client.wekan.lists, list_)
            print "move card from '%s' -> '%s'" % (before_list["title"],
                                                   after_list["title"])

        if card["archived"] != pr["closed"]:
            if pr["closed"]:
                print "archiving the card"
            else:
                print "card is re-opened"
            card["archived"] = pr["closed"]

        if card["userId"] != user:
            print "card has change of author for a weird reason, update it"
            card["userId"] = user

        if card.get("description", "") != pr["url"]:
            print "update descript from '%s' to '%s'" % (card.get(
                "description", ""), pr["url"])
            card["description"] = pr["url"]

        client.wekan.cards.update({"_id": card["_id"]}, {"$set": card})
def get_list_for_milestone(client, board, project, milestone):
    def update_list(list_, milestone):
        if list_["title"] != milestone_title:
            print "Updating milestone 'title from '%s' to '%s'" % (
                list_["title"], milestone_title)
            list_["title"] = milestone_title
            client.wekan.lists.update({"_id": list_["_id"]}, {"$set": list_})

        archived = milestone["state"] == "CLOSED"
        if list_["archived"] != archived:
            if archived:
                print "Archiving milestone '%s'" % (list_['title'])
            else:
                print "Briging milestone '%s' back from archives" % (
                    list_['title'])
            list_["archived"] = archived
            client.wekan.lists.update({"_id": list_["_id"]}, {"$set": list_})

    milestone_title = "%s [MILESTONE]" % milestone["title"]

    bridge_milestone = get_none(client.wekan.bridge_for_milestones, {
        "github_id": milestone["number"],
        "github_project": project
    })

    if bridge_milestone:
        print "Found bridge_milestone, return list"
        list_ = get_by_id(client.wekan.lists, bridge_milestone["wekan_id"])
        update_list(list_, milestone)
        return list_["_id"]

    # let's try to find an existing colum with the milestone name
    list_ = get_none(client.wekan.lists, {"title": milestone_title})

    # create it
    if not list_:
        sort = 1 + max([
            x.get("sort", 0)
            for x in client.wekan.lists.find({"boardId": board["_id"]})
        ])

        print "create new list '%s'" % milestone_title
        list_ = client.wekan.lists.insert({
            "_id":
            generate_id(),
            "title":
            milestone_title,
            "boardId":
            board["_id"],
            "archived":
            milestone["state"].upper() == "CLOSED",
            "createdAt":
            datetime.now(),
            "sort":
            sort
        })
    else:
        update_list(list_, milestone)
        list_ = list_["_id"]
        print "List exists for milestone '%s', return it" % milestone["title"]

    print "Create bridge for milestone %s -> %s" % (milestone["number"], list_)
    bridge_milestone = client.wekan.bridge_for_milestones.insert({
        "github_id":
        milestone["number"],
        "github_project":
        project,
        "wekan_id":
        list_
    })

    return list_