Skip to content

Commit 076a10d

Browse files
committed
PyLadies GitHub Bot
- Automatically create the label 🤖 feed/events label - Automatically post to Slack whenever an issue is labeled as 🤖 feed/events
1 parent 1796f09 commit 076a10d

File tree

9 files changed

+207
-1
lines changed

9 files changed

+207
-1
lines changed

Procfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
web: python3 -m webservice

README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,20 @@
11
# pyladies-bot
2-
PyLadies bot
2+
3+
PyLadies GitHub and Slack Bot.
4+
5+
## Features:
6+
7+
### For the pyladies/info repo:
8+
9+
1. Upon installation to a repo, the bot will add the `:robot: feed/events` label to the repo.
10+
11+
2. Whenever an issue is labeled with `:robot feed/events`, the issue will be copied over to
12+
PyLadies Slack under #events channel.
13+
14+
### TBD
15+
16+
Heroku web hosting paid for by @Mariatta through funds received from her GitHub Sponsors.
17+
[Sponsor Mariatta](https://github.com/sponsors/Mariatta) on GitHub.
18+
19+
20+

app.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"name": "PyLadies GitHub Bot",
3+
"description": "GitHub Bot for PyLadies",
4+
"repository": "https://github.com/pyladies/pyladies-github-bot"
5+
}

requirements.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
aiohttp
2+
gidgethub>=4.1.0
3+
PyJWT
4+
cryptography
5+
cachetools
6+
slackclient

runtime.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
python-3.8.3

webservice/__main__.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import asyncio
2+
import os
3+
import sys
4+
import traceback
5+
6+
7+
import aiohttp
8+
from aiohttp import web
9+
import cachetools
10+
from gidgethub import aiohttp as gh_aiohttp
11+
from gidgethub import routing
12+
from gidgethub import sansio
13+
from gidgethub import apps
14+
15+
from webservice import issues, installation
16+
17+
18+
cache = cachetools.LRUCache(maxsize=500)
19+
20+
routes = web.RouteTableDef()
21+
22+
router = routing.Router(issues.router, installation.router)
23+
24+
25+
@routes.get("/", name="home")
26+
async def handle_get(request):
27+
return web.Response(text="Hello world")
28+
29+
30+
@routes.post("/webhook")
31+
async def webhook(request):
32+
try:
33+
body = await request.read()
34+
secret = os.environ.get("GH_SECRET")
35+
event = sansio.Event.from_http(request.headers, body, secret=secret)
36+
if event.event == "ping":
37+
return web.Response(status=200)
38+
async with aiohttp.ClientSession() as session:
39+
gh = gh_aiohttp.GitHubAPI(session, "demo", cache=cache)
40+
41+
await asyncio.sleep(1)
42+
await router.dispatch(event, gh)
43+
try:
44+
print("GH requests remaining:", gh.rate_limit.remaining)
45+
except AttributeError:
46+
pass
47+
return web.Response(status=200)
48+
except Exception as exc:
49+
traceback.print_exc(file=sys.stderr)
50+
return web.Response(status=500)
51+
52+
53+
if __name__ == "__main__": # pragma: no cover
54+
app = web.Application()
55+
56+
app.router.add_routes(routes)
57+
port = os.environ.get("PORT")
58+
if port is not None:
59+
port = int(port)
60+
web.run_app(app, port=port)

webservice/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ROBOT_FEED_EVENTS_LABEL = ":robot: feed/events"

webservice/installation.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import os
2+
3+
4+
import gidgethub.routing
5+
6+
7+
router = gidgethub.routing.Router()
8+
9+
10+
from gidgethub import apps
11+
12+
13+
from webservice.constants import ROBOT_FEED_EVENTS_LABEL
14+
15+
16+
@router.register("installation", action="created")
17+
async def repo_installation_added(event, gh, *args, **kwargs):
18+
""" Create the #feed/events label in the repo where it gets installed """
19+
installation_id = event.data["installation"]["id"]
20+
installation_access_token = await apps.get_installation_access_token(
21+
gh,
22+
installation_id=installation_id,
23+
app_id=os.environ.get("GH_APP_ID"),
24+
private_key=os.environ.get("GH_PRIVATE_KEY"),
25+
)
26+
for repo in event.data["repositories"]:
27+
await handle_installed_repo_event(
28+
gh,
29+
installation_access_token,
30+
repo,
31+
installed_by=event.data["sender"]["login"],
32+
)
33+
34+
35+
@router.register("installation_repositories", action="added")
36+
async def repo_installation_added(event, gh, *args, **kwargs):
37+
""" Create the #feed/events label in the repo where it gets installed """
38+
installation_id = event.data["installation"]["id"]
39+
installation_access_token = await apps.get_installation_access_token(
40+
gh,
41+
installation_id=installation_id,
42+
app_id=os.environ.get("GH_APP_ID"),
43+
private_key=os.environ.get("GH_PRIVATE_KEY"),
44+
)
45+
for repo in event.data["repositories_added"]:
46+
await handle_installed_repo_event(
47+
gh,
48+
installation_access_token,
49+
repo,
50+
installed_by=event.data["sender"]["login"],
51+
)
52+
53+
54+
async def handle_installed_repo_event(gh, access_token, repo, installed_by):
55+
repo_full_name = repo["full_name"]
56+
57+
response = await gh.post(
58+
f"/repos/{repo_full_name}/labels",
59+
data={
60+
"name": ROBOT_FEED_EVENTS_LABEL,
61+
"description": "Automatically share this issue to PyLadies Slack #events channel",
62+
},
63+
oauth_token=access_token["token"],
64+
)
65+
label_url = response["url"]
66+
67+
url = f"/repos/{repo_full_name}/issues"
68+
response = await gh.post(
69+
url,
70+
data={
71+
"title": "Thanks for installing the PyLadies GitHub App",
72+
"body": f"Thanks @{installed_by}! I've created the label **[[:robot: feed-events]({label_url})]** in this repository.",
73+
},
74+
oauth_token=access_token["token"],
75+
)
76+
issue_url = response["url"]
77+
await gh.patch(
78+
issue_url, data={"state": "closed"}, oauth_token=access_token["token"],
79+
)

webservice/issues.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import os
2+
3+
4+
import gidgethub.routing
5+
6+
7+
router = gidgethub.routing.Router()
8+
from slack import WebClient
9+
from slack.errors import SlackApiError
10+
11+
12+
from webservice.constants import ROBOT_FEED_EVENTS_LABEL
13+
14+
15+
@router.register("issues", action="labeled")
16+
async def issue_labeled(event, gh, *args, **kwargs):
17+
for label in event.data["issue"]["labels"]:
18+
if label["name"] == ROBOT_FEED_EVENTS_LABEL:
19+
issue_title = event.data["issue"]["title"]
20+
issue_body = event.data["issue"]["body"]
21+
html_url = event.data["issue"]["html_url"]
22+
23+
# post it to Slack "events" channel
24+
slack_client = WebClient(token=os.getenv("SLACK_API_KEY"))
25+
try:
26+
response = slack_client.chat_postMessage(
27+
channel="#events",
28+
text=f"*New Event posted*\n\n*Title*: {issue_title}\n{issue_body}\n\n_source:_ _{html_url}_ ",
29+
)
30+
except SlackApiError as e:
31+
# You will get a SlackApiError if "ok" is False
32+
assert e.response["ok"] is False
33+
assert e.response[
34+
"error"
35+
] # str like 'invalid_auth', 'channel_not_found'

0 commit comments

Comments
 (0)