diff --git a/buildbot_transifex/webhook.py b/buildbot_transifex/webhook.py index f2c4864..6ed87d8 100644 --- a/buildbot_transifex/webhook.py +++ b/buildbot_transifex/webhook.py @@ -1,8 +1,13 @@ +"""Transifex webhook handler """ import base64 -import json -import re -import hmac import hashlib +import hmac +import json +import os +import requests +import re +import time + from buildbot.process.properties import Properties from buildbot.util import bytes2unicode, unicode2bytes from buildbot.www.hooks.base import BaseHookHandler @@ -10,134 +15,95 @@ from buildbot.www.hooks.base import BaseHookHandler from twisted.internet import defer from twisted.python import log -from dateutil.parser import parse as dateparse _HEADER_USER_AGENT = 'User-Agent' _HEADER_SIGNATURE = 'X-TX-Signature' +_HEADER_URL_PATH = 'X-TX-Url' +_HTTP_DATE = 'date' _EVENT_KEY = 'event' +author = 'buildbot-transifex' class TransifexHandler(BaseHookHandler): - def process_translation_completed(self, payload, event_type, codebase): - refname = payload["ref"] + def __init__(self, master, secret, transifex_to_github_map, options=None): + if not options: + options = {} + self.secret = secret + self.master = master + self.options = options + self.transifex_to_github_map = transifex_to_github_map + + def returnMessage(self, status = False, message = "Unimplemented"): + output = json.dumps({"status": "OK" if status else "FAIL", "message": message}) + return [output, [('Content-type', 'application/json')]] + + def _verifyTransifexSignature( + self, request, content, signature, header_signature + ): + http_verb = 'POST' + http_url_path = request.getHeader(_HEADER_URL_PATH) + http_gmt_date = request.getHeader(_HTTP_DATE) + content_md5 = hashlib.md5(content).hexdigest() + msg = b'\n'.join([ + http_verb, http_url_path, http_gmt_date, content_md5 + ]) + tx_signature = base64.b64encode( + hmac.new( + key=self.rendered_secret, + msg=msg, + digestmod=hashlib.sha256 + ).digest() + ) + if tx_signature != header_signature: + raise ValueError("Tx Signature mismatch") + + if signature != request.getHeader(_HEADER_SIGNATURE): + raise ValueError("Signature mismatch") + return True + + def process_translation_completed(self, payload, codebase): changes = [] - - # We only care about regular heads or tags - match = re.match(r"^refs/(heads|tags)/(.+)$", refname) - if not match: - log.msg("Ignoring refname '{}': Not a branch or tag".format(refname)) - return changes - - branch = match.group(2) - - repository = payload['repository'] - repo_url = repository['ssh_url'] - project = repository['full_name'] - - commits = payload['commits'] - if isinstance(self.options, dict) and self.options.get('onlyIncludePushCommit', False): - commits = commits[:1] - - for commit in commits: - files = [] - for kind in ('added', 'modified', 'removed'): - files.extend(commit.get(kind, []) or []) - timestamp = dateparse(commit['timestamp']) - change = { - 'author': '{} <{}>'.format(commit['author']['name'], - commit['author']['email']), - 'files': files, - 'comments': commit['message'], - 'revision': commit['id'], - 'when_timestamp': timestamp, - 'branch': branch, - 'revlink': commit['url'], - 'repository': repo_url, - 'project': project, - 'category': event_type, - 'properties': { - 'event': event_type, - 'repository_name': repository['name'], - 'owner': repository["owner"]["username"] - }, - } - if codebase is not None: - change['codebase'] = codebase - changes.insert(0, change) - return changes - - def process_review_compoleted(self, payload, event_type, codebase): - action = payload['action'] - - # Only handle potential new stuff, ignore close/. - # Merge itself is handled by the regular branch push message - if action not in ['opened', 'synchronized', 'edited', 'reopened']: - log.msg("Gitea Pull Request event '{}' ignored".format(action)) - return [] - pull_request = payload['pull_request'] - if not pull_request['mergeable']: - log.msg("Gitea Pull Request ignored because it is not mergeable.") - return [] - if pull_request['merged']: - log.msg("Gitea Pull Request ignored because it is already merged.") - return [] - timestamp = dateparse(pull_request['updated_at']) - base = pull_request['base'] - head = pull_request['head'] - repository = payload['repository'] + translated_request = self._transform_variables(payload['project'], payload['resource']) + ts = int(time.time()) change = { - 'author': '{} <{}>'.format(pull_request['user']['full_name'], - pull_request['user']['email']), - 'comments': 'PR#{}: {}\n\n{}'.format( - pull_request['number'], - pull_request['title'], - pull_request['body']), - 'revision': base['sha'], - 'when_timestamp': timestamp, - 'branch': base['ref'], - 'revlink': pull_request['html_url'], - 'repository': base['repo']['ssh_url'], - 'project': repository['full_name'], - 'category': event_type, + 'author': author, + 'branch': translated_request["branch"], + 'branch': translated_request["repository"], + 'project': translated_request["project"], 'properties': { - 'event': event_type, - 'base_branch': base['ref'], - 'base_sha': base['sha'], - 'base_repo_id': base['repo_id'], - 'base_repository': base['repo']['clone_url'], - 'base_git_ssh_url': base['repo']['ssh_url'], - 'head_branch': head['ref'], - 'head_sha': head['sha'], - 'head_repo_id': head['repo_id'], - 'head_repository': head['repo']['clone_url'], - 'head_git_ssh_url': head['repo']['ssh_url'], - 'head_owner': head['repo']['owner']['username'], - 'head_reponame': head['repo']['name'], - 'pr_id': pull_request['id'], - 'pr_number': pull_request['number'], - 'repository_name': repository['name'], - 'owner': repository["owner"]["username"], - }, + "transifex_language": payload.get("language", "None"), + "transifex_event": payload.get("event", "None"), + "transifex_project": payload.get("project", "None"), + "transifex_resource": payload.get("resource", "None"), + "transifex_branch": "translate_" + payload['language'] + "_" + str(ts) + } } if codebase is not None: change['codebase'] = codebase - return [change] + changes.insert(0, change) + return changes - def _transform_variables(payload): - retval = { - project: payload.get('project'), - repository = [payload.get('resource')], - branch = payload.get('language') + def _transform_variables(self, transifex_project): + if transifex_project is None: + raise ValueError("Unknown project %s from transifex".format(transifex_project)) + key = transifex_project + _map = self.map[key] + repository = _map["repository"] + project = re.sub(r'^.*/(.*?)(\.git)?$', r'\1', repository) + return{ + 'project': project, + 'repository': repository, + 'branch': _map["branch"], } - return retval @defer.inlineCallbacks def getChanges(self, request): - secret = None + change = {} + self.secret = None if isinstance(self.options, dict): - secret = self.options.get('secret') + self.secret = self.options.get('secret') try: content = request.content.read() content_text = bytes2unicode(content) @@ -145,36 +111,19 @@ class TransifexHandler(BaseHookHandler): except Exception as exception: raise ValueError('Error loading JSON: ' + str(exception)) - - if secret is not None: + if self.secret is not None: p = Properties() p.master = self.master - rendered_secret = yield p.render(secret) + option = self.options + rendered_secret = yield p.render(self.secret) signature = hmac.new( unicode2bytes(rendered_secret), unicode2bytes(content_text.strip()), digestmod=hashlib.sha256) header_signature = bytes2unicode( request.getHeader(_HEADER_SIGNATURE)) - - http_verb = 'POST' - http_url_path = request.headers('X-TX-Url') - http_gmt_date = request.headers('Date') - content_md5 = hashlib.md5(content).hexdigest() - msg = b'\n'.join([ - http_verb, http_url_path, http_gmt_date, content_md5 - ]) - tx_signature = base64.b64encode( - hmac.new( - key=rendered_secret, - msg=msg, - digestmod=hashlib.sha256 - ).digest() - ) - if tx_signature() != header_signature: - raise ValueError('Invalid secret') - - event_type = bytes2unicode(payload.get(_EVENT_KEY), "None") + self._verifyTransifexSignature(request, content, rendered_secret, signature, header_signature) + event_type = payload.get("event", "None") log.msg("Received event '{}' from transifex".format(event_type)) codebase = "" @@ -188,6 +137,4 @@ class TransifexHandler(BaseHookHandler): return (changes, 'transifex') - -# Plugin name transifex = TransifexHandler diff --git a/setup.py b/setup.py index 7f030f6..84a0eb0 100644 --- a/setup.py +++ b/setup.py @@ -6,32 +6,23 @@ from setuptools import setup with open("README.md", "r") as fh: long_description = fh.read() -VERSION = "1.7.2" +VERSION = "0.1" -setup(name='buildbot-gitea', +setup(name='buildbot-transifex', version=VERSION, - description='buildbot plugin for integration with Gitea.', - author='Marvin Pohl', - author_email='hello@lab132.com', - url='https://github.com/lab132/buildbot-gitea', - long_description=long_description, + description='buildbot plugin for integration with transifex.', + author='', + author_email='', + url='', + long_description='Transifex webhook', long_description_content_type="text/markdown", - packages=['buildbot_gitea'], + packages=['buildbot_transifex'], install_requires=[ "buildbot>=3.0.0" ], entry_points={ "buildbot.webhooks": [ - "gitea = buildbot_gitea.webhook:gitea" - ], - "buildbot.steps": [ - "Gitea = buildbot_gitea.step_source:Gitea" - ], - "buildbot.reporters": [ - "GiteaStatusPush = buildbot_gitea.reporter:GiteaStatusPush" - ], - "buildbot.util": [ - "GiteaAuth = buildbot_gitea.auth:GiteaAuth" + "transifex = buildbot_transifex.webhook:transifex" ] }, classifiers=[