Skip to content

Commit

Permalink
github: add tag support to create/preview to add labels to issues
Browse files Browse the repository at this point in the history
  • Loading branch information
snarfed committed Jul 5, 2018
1 parent 38b1208 commit aa775fe
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 54 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ Changelog
* Instagram:
* Add global rate limiting lock for scraping. If a scraping HTTP request gets a 429 or 503 response, we refuse to make more requests for 5m, and instead short circuit and return the same error. This can be overridden with a new `ignore_rate_limit` kwarg to `get_activities()`.
* GitHub:
* Add `tag` support to `create`/`preview_create` to add label(s) to existing issues ([snarfed/bridgy#811](https://github.com/snarfed/bridgy/issues/811)).
* Escape HTML characters (`<`, `>`, and `&`) in content in `create()` and `preview_create()` ([snarfed/bridgy#810](https://github.com/snarfed/bridgy/issues/810)).
* `get_activities()` and `get_comment()` now return `ValueError` instead of `AssertionError` on malformed `activity_id` and `comment_id` args, respectively.
* `get_activities()` bug fix for issues/PRs with no body text.
Expand Down
143 changes: 92 additions & 51 deletions granary/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,8 @@ def _create(self, obj, preview=None, include_link=source.OMIT_LINK,
assert preview in (False, True)

type = source.object_type(obj)
if type and type not in ('issue', 'comment', 'activity', 'note', 'article', 'like'):
if type and type not in ('issue', 'comment', 'activity', 'note', 'article',
'like', 'tag'):
return source.creation_result(
abort=False, error_plain='Cannot publish %s to GitHub' % type)

Expand All @@ -511,57 +512,23 @@ def _create(self, obj, preview=None, include_link=source.OMIT_LINK,
parsed = urllib.parse.urlparse(base_url)
path = parsed.path.strip('/').split('/')
owner, repo = path[:2]
if len(path) == 4:
number = path[3]

comment_id = re.match(r'^issuecomment-([0-9]+)$', parsed.fragment)
if comment_id:
comment_id = comment_id.group(1)
elif parsed.fragment:
return source.creation_result(
abort=True,
error_plain='You need an in-reply-to GitHub repo, issue, PR, or comment URL.')
error_plain='Please remove the fragment #%s from your in-reply-to URL.' %
parsed.fragment)

if len(path) == 2 or (len(path) == 3 and path[2] == 'issues'):
if type == 'like': # star
if preview:
return source.creation_result(description="""\
<span class="verb">star</span> <a href="%s">%s/%s</a>.""" %
(base_url, owner, repo))
else:
issue = self.graphql(GRAPHQL_REPO, locals())
resp = self.graphql(GRAPHQL_ADD_STAR, {
'starrable_id': issue['repository']['id'],
})
return source.creation_result({
'url': base_url + '/stargazers',
})
if type == 'comment': # comment or reaction
if not (len(path) == 4 and path[2] in ('issues', 'pull')):
return source.creation_result(
abort=True, error_plain='GitHub comment requires in-reply-to issue or PR URL.')

else: # new issue
title = util.ellipsize(obj.get('displayName') or obj.get('title') or
orig_content)
labels = self.existing_labels(obj.get('tags', []), owner, repo)

if preview:
preview_content = '<b>%s</b><hr>%s' % (
title, self.render_markdown(content, owner, repo))
preview_labels = ''
if labels:
preview_labels = ' and attempt to add label%s <span class="verb">%s</span>' % (
's' if len(labels) > 1 else '', ', '.join(labels))
return source.creation_result(content=preview_content, description="""\
<span class="verb">create a new issue</span> on <a href="%s">%s/%s</a>%s:""" %
(base_url, owner, repo, preview_labels))
else:
resp = self.rest(REST_API_CREATE_ISSUE % (owner, repo), {
'title': title,
'body': content,
'labels': labels,
}).json()
resp['url'] = resp.pop('html_url')
return source.creation_result(resp)

elif len(path) == 4 and path[2] in ('issues', 'pull'):
# comment or reaction
owner, repo, _, number = path
if comment_id:
comment = self.rest(REST_API_COMMENT % (owner, repo, comment_id)).json()
is_reaction = orig_content in REACTIONS_GRAPHQL
Expand Down Expand Up @@ -623,23 +590,97 @@ def _create(self, obj, preview=None, include_link=source.OMIT_LINK,
except ValueError as e:
return source.creation_result(abort=True, error_plain=str(e))

elif type == 'like': # star
if not (len(path) == 2 or (len(path) == 3 and path[2] == 'issues')):
return source.creation_result(
abort=True, error_plain='GitHub like requires in-reply-to repo URL.')

if preview:
return source.creation_result(
description='<span class="verb">star</span> <a href="%s">%s/%s</a>.' %
(base_url, owner, repo))
else:
issue = self.graphql(GRAPHQL_REPO, locals())
resp = self.graphql(GRAPHQL_ADD_STAR, {
'starrable_id': issue['repository']['id'],
})
return source.creation_result({
'url': base_url + '/stargazers',
})

elif type == 'tag': # add label
if not (len(path) == 4 and path[2] in ('issues', 'pull')):
return source.creation_result(
abort=True, error_plain='GitHub tag post requires tag-of issue or PR URL.')

tags = set(util.trim_nulls(t.get('displayName', '').strip()
for t in util.get_list(obj, 'object')))
if not tags:
return source.creation_result(
abort=True, error_plain='No tags found in tag post!')

existing_labels = self.existing_labels(owner, repo)
labels = sorted(tags & existing_labels)
issue_link = '<a href="%s">%s/%s#%s</a>' % (base_url, owner, repo, number)
if not labels:
return source.creation_result(
abort=True,
error_html="No tags in [%s] matched %s's existing labels [%s]." %
(', '.join(tags), issue_link, ', '.join(existing_labels)))

if preview:
return source.creation_result(
description='add label%s <span class="verb">%s</span> to %s.' % (
('s' if len(labels) > 1 else ''), ', '.join(labels), issue_link))
else:
resp = self.rest(REST_API_ISSUE_LABELS % (owner, repo, number), labels).json()
return source.creation_result(resp)

else: # new issue
if not (len(path) == 2 or (len(path) == 3 and path[2] == 'issues')):
return source.creation_result(
abort=True, error_plain='New GitHub issue requires in-reply-to repo URL')

title = util.ellipsize(obj.get('displayName') or obj.get('title') or
orig_content)
tags = set(util.trim_nulls(t.get('displayName', '').strip()
for t in util.get_list(obj, 'tags')))
labels = sorted(tags & self.existing_labels(owner, repo))

if preview:
preview_content = '<b>%s</b><hr>%s' % (
title, self.render_markdown(content, owner, repo))
preview_labels = ''
if labels:
preview_labels = ' and attempt to add label%s <span class="verb">%s</span>' % (
's' if len(labels) > 1 else '', ', '.join(labels))
return source.creation_result(content=preview_content, description="""\
<span class="verb">create a new issue</span> on <a href="%s">%s/%s</a>%s:""" %
(base_url, owner, repo, preview_labels))
else:
resp = self.rest(REST_API_CREATE_ISSUE % (owner, repo), {
'title': title,
'body': content,
'labels': labels,
}).json()
resp['url'] = resp.pop('html_url')
return source.creation_result(resp)

return source.creation_result(
abort=False,
error_plain="%s doesn't look like a GitHub repo, issue, or PR URL." % base_url)

def existing_labels(self, tags, owner, repo):
"""Returns a subset of tags that exist as labels on a repo.
def existing_labels(self, owner, repo):
"""Fetches and returns a repo's labels.
Args:
tags: sequence of strings
owner: string, GitHub username or org that owns the repo
repo: string
Returns: set of strings
"""
labels_resp = self.graphql(GRAPHQL_REPO_LABELS, locals())
existing_labels = set(node['name'] for node in
labels_resp['repository']['labels']['nodes'])
labels = set(util.trim_nulls(t.get('displayName', '').strip() for t in tags))
return sorted(labels & existing_labels)
resp = self.graphql(GRAPHQL_REPO_LABELS, locals())
return set(node['name'] for node in resp['repository']['labels']['nodes'])

@classmethod
def issue_to_object(cls, issue):
Expand Down
59 changes: 57 additions & 2 deletions granary/test/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
REST_API_COMMENT_REACTIONS,
REST_API_COMMENTS,
REST_API_ISSUE,
REST_API_ISSUE_LABELS,
REST_API_NOTIFICATIONS,
REST_API_REACTIONS,
)
Expand Down Expand Up @@ -321,6 +322,15 @@ def tag_uri(name):
'verb': 'like',
'object': {'url': 'https://github.com/foo/bar'},
}
TAG_ACTIVITY = {
'objectType': 'activity',
'verb': 'tag',
'object': [
{'displayName': 'one'},
{'displayName': 'three'},
],
'target': {'url': 'https://github.com/foo/bar/issues/456'},
}
NOTIFICATION_PULL_REST = { # GitHub
'id': '302190598',
'unread': False,
Expand All @@ -346,6 +356,8 @@ def tag_uri(name):
'Authorization': 'token a-towkin',
'Accept': 'application/vnd.github.squirrel-girl-preview+json',
}


class GitHubTest(testutil.TestCase):

def setUp(self):
Expand Down Expand Up @@ -842,7 +854,7 @@ def test_create_in_reply_to_bad_fragment(self):
for fn in (self.gh.preview_create, self.gh.create):
result = fn(obj)
self.assertTrue(result.abort)
self.assertIn('You need an in-reply-to GitHub repo, issue, PR, or comment URL.',
self.assertIn('Please remove the fragment #bad-456 from your in-reply-to URL.',
result.error_plain)

def test_create_star(self):
Expand Down Expand Up @@ -932,4 +944,47 @@ def test_preview_reaction_comment(self):
self.mox.ReplayAll()

preview = self.gh.preview_create(COMMENT_REACTION_OBJ_INPUT)
self.assertEquals(u'<span class="verb">react 👍</span> to <a href="https://github.com/foo/bar/pull/123#issuecomment-456">a comment on foo/bar#123, <em>i have something to say here</em></a>.', preview.description)
self.assertEquals(u'<span class="verb">react 👍</span> to <a href="https://github.com/foo/bar/pull/123#issuecomment-456">a comment on foo/bar#123, <em>i have something to say here</em></a>.', preview.description, preview)

def test_create_add_label(self):
self.expect_graphql_get_labels(['one', 'two'])
resp = {
'id': 'DEF456',
'node_id': 'MDU6TGFiZWwyMDgwNDU5NDY=',
'name': 'an issue',
# ¯\_(ツ)_/¯ https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue
'default': True,
}
self.expect_requests_post(REST_API_ISSUE_LABELS % ('foo', 'bar', 456),
headers=EXPECTED_HEADERS, json=['one'], response=resp)
self.mox.ReplayAll()

result = self.gh.create(TAG_ACTIVITY)
self.assert_equals(resp, result.content, result)

def test_preview_add_label(self):
self.expect_graphql_get_labels(['one', 'two'])
self.mox.ReplayAll()

preview = self.gh.preview_create(TAG_ACTIVITY)
self.assertIsNone(preview.error_plain, preview)
self.assertEquals(
'add label <span class="verb">one</span> to <a href="https://github.com/foo/bar/issues/456">foo/bar#456</a>.',
preview.description, preview)

def test_create_add_label_no_tags(self):
activity = copy.deepcopy(TAG_ACTIVITY)
activity['object'] = []
result = self.gh.create(activity)
self.assertTrue(result.abort)
self.assertEquals('No tags found in tag post!', result.error_plain)

def test_create_add_label_no_matching(self):
self.expect_graphql_get_labels(['one', 'two'])
self.mox.ReplayAll()

activity = copy.deepcopy(TAG_ACTIVITY)
activity['object'] = [{'displayName': 'three'}]
result = self.gh.create(activity)
self.assertTrue(result.abort)
self.assertEquals("""No tags in [three] matched <a href="https://github.com/foo/bar/issues/456">foo/bar#456</a>'s existing labels [two, one].""", result.error_html, result)
2 changes: 1 addition & 1 deletion granary/twitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ def get_activities_response(self, user_id=None, group_id=None, app_id=None,
get_activities(user_id='exampleuser', group_id='example-list').
Twitter replies default to including a mention of the user they're replying
to, which overloads mentions a bit. When fetch_shares is True, we determine
to, which overloads mentions a bit. When fetch_mentions is True, we determine
that a tweet mentions the current user if it @-mentions their username and:
* it's not a reply, OR
Expand Down

0 comments on commit aa775fe

Please sign in to comment.