diff --git a/bugwarrior/services/mantisbt.py b/bugwarrior/services/mantisbt.py
new file mode 100644
index 0000000..e54af0d
--- /dev/null
+++ b/bugwarrior/services/mantisbt.py
@@ -0,0 +1,361 @@
+from builtins import filter
+import re
+import six
+
+import requests
+from jinja2 import Template
+
+from bugwarrior.config import asbool, aslist, die
+from bugwarrior.services import IssueService, Issue, ServiceClient
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class MantisbtClient(ServiceClient):
+ def __init__(self, host, token):
+ self.host = host
+ self.session = requests.Session()
+ self.session.headers['Authorization'] = token
+
+ def _api_url(self, path, **context):
+ """ Build the full url to the API endpoint """
+ baseurl = "https://{}/api/rest".format(self.host)
+ return baseurl + path.format(**context)
+
+ def get_user(self):
+ return self.json_response(self.session.get(self._api_url("/users/me")))
+
+ def get_projects(self):
+ return self._getter(self._api_url("/projects"), subkey="projects")
+
+ def get_issues(self):
+ url = self._api_url("/issues?page_size=50")
+ return self._getter(url, page_size=50, subkey="issues")
+
+ def get_assigned_issues(self):
+ """ Returns all issues assigned to authenticated user.
+ """
+ url = self._api_url("/issues?page_size=50&filter_id=assigned")
+ return self._getter(url, page_size=50, subkey="issues")
+
+ def get_monitored_issues(self):
+ """ Returns all issues monitored by authenticated user.
+ """
+ url = self._api_url("/issues?page_size=50&filter_id=monitored")
+ return self._getter(url, page_size=50, subkey="issues")
+
+ def get_reported_issues(self):
+ """ Returns all issues reported by authenticated user.
+ """
+ url = self._api_url("/issues?page_size=50&filter_id=reported")
+ return self._getter(url, page_size=50, subkey="issues")
+
+ def _getter(self, url, page_size=None, subkey=None):
+ """ Pagination utility. Obnoxious. """
+
+ results = []
+ link = dict(next=url)
+ page_number = 1
+
+ while 'next' in link:
+ if page_size is not None:
+ response = self.session.get(link['next'] + "&page=" + str(page_number))
+ else:
+ response = self.session.get(link['next'])
+
+ json_res = self.json_response(response)
+
+ if subkey is not None:
+ json_res = json_res[subkey]
+
+ results += json_res
+
+ if page_size is not None and len(json_res) == page_size:
+ page_number += 1
+ else:
+ break
+
+ return results
+
+class MantisbtIssue(Issue):
+ TITLE = 'mantisbttitle'
+ BODY = 'mantisbtbody'
+ CREATED_AT = 'mantisbtcreatedon'
+ UPDATED_AT = 'mantisbtupdatedat'
+ CLOSED_AT = 'mantisbtclosedon'
+ URL = 'mantisbturl'
+ PROJECT = 'mantisbtproject'
+ NUMBER = 'mantisbtnumber'
+ USER = 'mantisbtuser'
+ CATEGORY = 'mantisbtcategory'
+ STATE = 'mantisbtstate'
+
+ UDAS = {
+ TITLE: {
+ 'type': 'string',
+ 'label': 'Mantisbt Title',
+ },
+ BODY: {
+ 'type': 'string',
+ 'label': 'Mantisbt Body',
+ },
+ CREATED_AT: {
+ 'type': 'date',
+ 'label': 'Mantisbt Created',
+ },
+ UPDATED_AT: {
+ 'type': 'date',
+ 'label': 'Mantisbt Updated',
+ },
+ CLOSED_AT: {
+ 'type': 'date',
+ 'label': 'Mantisbt Closed',
+ },
+ PROJECT: {
+ 'type': 'string',
+ 'label': 'Mantisbt Project',
+ },
+ URL: {
+ 'type': 'string',
+ 'label': 'Mantisbt URL',
+ },
+ NUMBER: {
+ 'type': 'numeric',
+ 'label': 'Mantisbt Issue #',
+ },
+ USER: {
+ 'type': 'string',
+ 'label': 'Mantisbt User',
+ },
+ CATEGORY: {
+ 'type': 'string',
+ 'label': 'Mantisbt Category',
+ },
+ STATE: {
+ 'type': 'string',
+ 'label': 'Mantisbt State',
+ }
+ }
+ UNIQUE_KEY = (URL, NUMBER, )
+
+ def _normalize_tag(self, label):
+ return re.sub(r'[^a-zA-Z0-9]', '_', label)
+
+ def to_taskwarrior(self):
+ body = self.record.get('description')
+ if body:
+ body = body.replace('\r\n', '\n')
+
+ created = self.parse_date(self.record.get('created_at'))
+ updated = self.parse_date(self.record.get('updated_at'))
+ closed_date = None
+ if self.record["status"]["name"] in ["closed", "resolved"]:
+ for history in self.record.get("history", []):
+ if history.get("field", {}).get("name", "") == "status"\
+ and history.get("new_value", {}).get("name", "") in ["closed", "resolved"]:
+ closed_date = history["created_at"]
+ closed = self.parse_date(closed_date)
+
+ return {
+ 'project': self.record['project']['name'],
+ 'priority': self.origin['default_priority'],
+ 'annotations': self.get_annotations(),
+ 'tags': self.get_tags(),
+ 'entry': created,
+ 'end': closed,
+
+ self.TITLE: self.record.get('summary'),
+ self.BODY: body,
+ self.CREATED_AT: created,
+ self.UPDATED_AT: updated,
+ self.CLOSED_AT: closed,
+ self.URL: self.get_url(),
+ self.PROJECT: self.record['project'].get('name'),
+ self.NUMBER: self.record['id'],
+ self.USER: self.record['reporter'].get('name'),
+ self.CATEGORY: self.record['category'].get('name'),
+ self.STATE: self.record['status'].get('label'),
+ }
+
+ def get_url(self):
+ return "https://{}view.php?id={}".format(self.extra['host'], self.record["id"])
+
+ def get_annotations(self):
+ annotations = []
+
+ context = self.record.copy()
+ annotation_template = Template(self.origin['annotation_template'])
+
+ for annotation_dict in self.record.get('notes', []):
+ context.update({
+ 'text': annotation_dict['text'],
+ 'date': annotation_dict['created_at'],
+ 'author': annotation_dict['reporter'].get('name', 'unknown'),
+ 'view': annotation_dict['view_state']['label'],
+ })
+ annotations.append(
+ annotation_template.render(context)
+ )
+ return annotations
+
+ def get_tags(self):
+ tags = []
+
+ context = self.record.copy()
+ tag_template = Template(self.origin['tag_template'])
+
+ for tag_dict in self.record.get('tags', []):
+ context.update({
+ 'tag': self._normalize_tag(tag_dict['name'])
+ })
+ tags.append(
+ tag_template.render(context)
+ )
+
+ return tags
+
+ def get_default_description(self):
+ return self.build_default_description(
+ title=self.record['summary'],
+ url=self.get_processed_url(self.get_url()),
+ number=self.record['id'],
+ )
+
+
+class MantisbtService(IssueService):
+ ISSUE_CLASS = MantisbtIssue
+ CONFIG_PREFIX = 'mantisbt'
+
+ def __init__(self, *args, **kw):
+ super(MantisbtService, self).__init__(*args, **kw)
+
+ self.host = self.config.get('host', 'www.mantisbt.org/bugs/')
+
+ token = self.get_password('token')
+
+ self.client = MantisbtClient(self.host, token)
+ self.user = None
+
+ self.exclude_projects = self.config.get('exclude_projects', [], aslist)
+ self.include_projects = self.config.get('include_projects', [], aslist)
+
+ self.involved_issues = self.config.get(
+ 'involved_issues', default=True, to_type=asbool
+ )
+ self.assigned_issues = self.config.get(
+ 'assigned_issues', default=False, to_type=asbool
+ )
+ self.monitored_issues = self.config.get(
+ 'monitored_issues', default=False, to_type=asbool
+ )
+ self.reported_issues = self.config.get(
+ 'reported_issues', default=False, to_type=asbool
+ )
+ self.tag_template = self.config.get(
+ 'tag_template', default='{{tag}}', to_type=six.text_type
+ )
+ self.annotation_template = self.config.get(
+ 'annotation_template', default='{{date}} {{author}} ({{view}}): {{text}}', to_type=six.text_type
+ )
+
+ def get_service_metadata(self):
+ return {
+ 'tag_template': self.tag_template,
+ 'annotation_template': self.annotation_template,
+ }
+
+ def filter_involved_issues(self, issue):
+ _, issue = issue
+ user = self.client.get_user()
+ uid = user["id"]
+ if issue["reporter"]["id"] != uid and \
+ issue.get("handler", {}).get("id") != uid and \
+ all([ x.get("user", {}).get("id") != uid for x in issue.get("history", [])]) and \
+ all([ x.get("user", {}).get("id") != uid for x in issue.get("monitors", [])]):
+ return False
+ return self.filter_project_name(issue["project"]["name"])
+
+ def filter_issues(self, issue):
+ _, issue = issue
+ return self.filter_project_name(issue["project"]["name"])
+
+ def filter_project_name(self, name):
+ if self.exclude_projects:
+ if name in self.exclude_projects:
+ return False
+
+ if self.include_projects:
+ if name in self.include_projects:
+ return True
+ else:
+ return False
+
+ return True
+
+ @staticmethod
+ def get_keyring_service(service_config):
+ host = service_config.get('host', 'www.mantisbt.org/bugs/')
+ username = service_config.get('username', default='nousername')
+ return "mantisbt://{username}@{host}".format(username=username,
+ host=host)
+
+ @staticmethod
+ def to_issue_dict(issues):
+ return { i['id']: i for i in issues }
+
+ def get_owner(self, issue):
+ return issue.get("handler", {}).get("name")
+
+ def get_author(self, issue):
+ return issue.get("reporter", {}).get("name")
+
+ def issues(self):
+ issues = {}
+ is_limited = self.assigned_issues or self.monitored_issues or self.reported_issues
+
+ if self.assigned_issues:
+ issues.update(
+ filter(self.filter_issues, self.to_issue_dict(self.client.get_assigned_issues()).items())
+ )
+ if self.monitored_issues:
+ issues.update(
+ filter(self.filter_issues, self.to_issue_dict(self.client.get_monitored_issues()).items())
+ )
+ if self.reported_issues:
+ issues.update(
+ filter(self.filter_issues, self.to_issue_dict(self.client.get_reported_issues()).items())
+ )
+
+ if not is_limited:
+ all_issues = self.to_issue_dict(self.client.get_issues())
+ if self.involved_issues:
+ issues.update(
+ filter(self.filter_involved_issues, all_issues.items())
+ )
+ else:
+ issues.update(
+ filter(self.filter_issues, all_issues.items())
+ )
+
+ log.debug(" Found %i issues.", len(issues))
+ if not is_limited:
+ issues = list(filter(self.include, issues.values()))
+ else:
+ issues = list(issues.values())
+ log.debug(" Pruned down to %i issues.", len(issues))
+
+ for issue in issues:
+ issue_obj = self.get_issue_for_record(issue)
+ extra = {
+ 'host': self.host
+ }
+ issue_obj.update_extra(extra)
+ yield issue_obj
+
+ @classmethod
+ def validate_config(cls, service_config, target):
+ if 'token' not in service_config:
+ die("[%s] has no 'mantisbt.token'" % target)
+
+ super(MantisbtService, cls).validate_config(service_config, target)
diff --git a/setup.py b/setup.py
index d6d957a..665e36e 100644
--- a/setup.py
+++ b/setup.py
@@ -80,6 +80,7 @@ setup(name='bugwarrior',
activecollab2=bugwarrior.services.activecollab2:ActiveCollab2Service
activecollab=bugwarrior.services.activecollab:ActiveCollabService
jira=bugwarrior.services.jira:JiraService
+ mantisbt=bugwarrior.services.mantisbt:MantisbtService
megaplan=bugwarrior.services.megaplan:MegaplanService
phabricator=bugwarrior.services.phab:PhabricatorService
versionone=bugwarrior.services.versionone:VersionOneService