đ Rocket 2¶
Rocket 2 is a from-the-ground-up rebuild of Rocket, UBC Launch Padâs in-house management Slack bot.
Developer Installation¶
We use pipenv for dependency management.
git clone https://github.com/ubclaunchpad/rocket2.git
cd rocket2/
pip install pipenv
pipenv install --dev
pipenv
will manage a virtualenv,
so interacting with the program or using the development tools has to be done
through pipenv, like so:
pipenv run pycodestyle .
This can get inconvenient, so you can instead create a shell that runs in the managed environment like so:
pipenv shell
and then commands like pycodestyle
and pytest
can be run like normal.
Additionally, we use Github Actions as a CI system. To run the same checks
locally, we provide scripts/build_check.sh
; this can be run with:
./scripts/build_check.sh
The above tests would be run with the assumption that other applications, such as a local instance of DynamoDB, is also running. To run tests that explicitly do not involve the running of any database, run pytest with the following arguments:
pytest -m "not db"
You can also install it as a pre-commit hook for git:
cd scripts/
make install
Note that testing alongside a real Slack workspace, DynamoDB, and so on requires quite a bit more setup. For a full guide to developer installation, see our local development guide.
Running DynamoDB Locally¶
Some tests assume the existence of a local DynamoDB database. These are primarily for automated testing, like on Github Actions CI, but if you would like to run them yourself or are developing new tests, you can run as follows:
wget https://s3-us-west-2.amazonaws.com/dynamodb-local/dynamodb_local_latest.tar.gz
mkdir DynamoDB
tar -xvf dynamodb_local_latest.tar.gz --directory DynamoDB
# Configure AWS
scripts/setup_localaws.sh
# Run DynamoDB through Java
cd DynamoDB/
java -Djava.library.path=./DynamoDBLocal_lib -jar DynamoDBLocal.jar -sharedDb
# Open a new terminal to continue interacting
For a more sandboxed approach, you can use Docker and docker-compose to spin up a local DynamoDB instance:
docker-compose -f sandbox.yml up
You can then point a Rocket instance at this DynamoDB database by setting
AWS_LOCAL=True
.
Contributing¶
This document contains important details for anyone contributing to Rocket 2.
Opening an Issue¶
If you see a bug or have a feature request, please open an issue! That being said, make sure to do a quick search first - there may already be an issue that covers it.
When creating a new issue, please add a label describing the issue; the most relevant are probably âBugâ and âFeature requestâ.
If you are going to work on an issue, please assign yourself to it, and unassign yourself if you stop working on it.
If you are not planning to work on a new issue, please also add it to the Rocket 2.0 project; this will automatically add it to our Kanban boardâs backlog, where we can review it in a future sprint.
Setting up branches¶
Before you make any changes, you should first set up your own branch. It is common convention to name your branch:
<username>/#<issue-number>-<description-of-fix>
So if your issue is #153 Read from configuration, you would name it
rwblickhan/#153-read-from-config
. The name needs to be concise, descriptive,
and, well, have your name and number, so to speak.
Before-Pull-Request checklist¶
- All tests and style and docs checks pass (
scripts/build_check.sh
) - The Github build passes (Github will build your commit when you push it)
- Your code is presentable and you have not committed extra files (think your credentials, IDE config files, cached directories, build directories, etc.)
- Youâve written unit tests for the changes youâve made, and that they cover all the code you wrote (or effectively all, given the circumstances)
We use codecov
to check code coverage, but you can easily check the code
coverage using the scripts/build_check.sh
script. The coverage should be
displayed after the unit tests are run.
Submitting a Pull Request¶
We appreciate pull requests of any size or scope.
Please use a clear, descriptive title for your pull request and fill out the pull request template with as much detail as you can. In particular, all pull requests should be linked to one or more issues - if a relevant issue does not exist, please create one as described above.
All pull requests must be code reviewed. Currently the code is owned by the brussel-sprouts team at UBC Launch Pad; at least one member of the team must approve the pull request before it can be merged.
All pull requests must pass our Github build before they can be merged. The Github build checks for:
- Passing unit tests (via pytest)
- Minimum code coverage of unit tests (via Codecov.io)
- Code linting (via flake8)
- PEP8 code style (via pycodestyle)
- Correctly-formatted docstrings (via pydocstyle)
- Correctly-formatted Markdown documentation (via mdl)
All of these checks are conveniently done using the scripts/build_check.sh
as
mentioned above.
Remember to add the label Ready for Review
.
After your pull request has been approved and the Github build passes, it can
be merged into master
. Please do so with an ordinary merge commit, not a
rebase or squash merge.
Work in progress (WIP) pull requests¶
Sometimes, it may be more appropriate to submit a pull request that you are
working on, just to say that you are working on something (or so that you can
get some initial feedback on your work). In that case, it can be a good idea to
submit a pull request marked WIP. The convention here is to prepend [WIP]
in
the title of the request, and to further mark it with the label WIP
.
Updating an Outdated Pull Request¶
If changes have been merged between when you started work on your branch and when your pull request was approved, you will have to update your branch. The preferred way to do so is with a rebase.
Assuming you are on your working branch:
git pull origin master
git rebase master
If you have changed files that were also changed in the intervening merge, git
rebase
may report merge conflicts. If this happens, donât panic! Use git
status
and git diff
to determine which files conflict and where, use an
editor to fix the conflicts, then stage the formerly-conflicting files with
git add FILE
. Finally, use git rebase --continue
to apply the fix and
continue rebasing. Note that you may have to fix conflicts multiple times in a
single rebase.
It is also a good idea to replace the label Ready for Review
with Ready for
Re-Review
for clarity.
License¶
MIT License
Copyright (c) 2018 UBC Launch Pad
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the âSoftwareâ), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED âAS ISâ, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Getting Started¶
Create an User Model for DynamoDB database¶
A quick guide run through how Rocket2 takes in a command to generate a model that will be stored onto the database.
So you just joined Launchpad and want to add yourself to Rocket2. You go on slack and starts to talk to the Rocket2 bot, but what should you say? To get started, hereâs a command you can enter:
command¶
A slack user calls Rocket2 to edit their information.
# SLACK_ID will be the current user's slack id.
# For this example, let's assume the slack id to be `StevenU`
/rocket user edit --name "Steven Universe" --email "su@gmail.com"
Yay! You have done what you were told to do, but wait! As a curious software developer, youâre curious about what makes Rocket2 tick. How exactly is your information saved onto Rocket2? Well, for every member added to Rocket2, a user model gets created.
model¶
An User model is constructed from the information the user input. Unfilled parameters will remain empty.
# To construct a User model with Slack ID 'StevenU'
steven_universe = User('StevenU')
steven_universe.email = 'su@gmail.com'
# To check if this user is valid.
User.is_valid(steven_universe) # returns true
# To get a user's permission level.
steven_universe.permissions_level # returns Permissions_member
Launchpad is growing every year, so there are a lot of user, hence a lot of user models. We have to be able to keep track and organize everyone, so thatâs where database comes in. We create a table for every type of model, so in this case weâll create a user table to store all users.
database (db)¶
Instead of using dynamodb.py
to handle our User model, we will use facade.py
so we avoid becoming dependent on a single database. In the future, this allows
us to easily switch to using other databases.
# To store an user into the database.
facade.store(steven_universe)
# To retrieve an user from the database.
facade.retrieve(User, 'StevenU') # returns steven_universe user model
# If we try to retrieve a non-existent: user, a LookupError will be thrown.
facade.retrieve(User, 'fakeU') # returns 'User fakeU not found'
# To query an user based on a parameter, a list of matching Users will be
returned.
facade.query(User, ['name', 'Steven Universe'] # returns [steven_universe]
# To query an user based on a non-existent parameter, an empty list will be
returned.
facade.query(User, ['email', 'fakeemail@gmail.com'] # returns []
# To query an user without parameters, all the users will be returned
facade.query(User, []) # returns [steven_universe, second_user]
Testing¶
Warning: This is no longer the most up-to-date documentation on how testing is done here. You may want to head over here for more up-to-date documentation on how we test things. You have been warnedâŠ.
Running Pytest Efficiently¶
Test Driven Development⊠we hear professors preach about it during lectures
but we never got an opportunity to put it to good use until Rocket2 came along.
Unfortunately we got over excited and wrote A LOT of tests. Running them all
every time is a bit painful, thatâs where @pytest.mark
comes in. pytest.mark
allows you to label your tests to run them in groups.
We only have tests that test the functions by themselves. Features that involve multiple parts (such as a new command involving Slack, Github, and the database) should be tested manually as well.
Run all the tests¶
pytest
Run only db tests¶
pytest -m db
Run all tests except database tests¶
pytest -m "not db"
Testing the Database¶
What are environment variables? Variables for the environment of course! These variables set up the environment for testing. Rocket2 uses them because we have both a local and a sever DynamoDB database and each require an extra variable to get everything working.
Run local DynamoDB¶
We use the AWS_LOCAL
environment variable to indicate if we want to run
DynamoDB locally or on a server. Change AWS_LOCAL = 'True'
to use local
DynamoDB.
If AWS_LOCAL == 'True'
but you did not start an instance of local DynamoDB,
scripts/build_check.sh
will automatically skip all database tests.
This is the recommended way for unit testing.
Run server DynamoDB¶
To run the server DynamoDB we need to set the AWS_REGION
and obtain
AWS_ACCESS_KEYID
, AWS_SECRET_KEY
, and GITHUB_KEY
.
This is the recommended way for testing everything (not unit testing, but testing the slack commands themselves). Click here to learn how to set up a full development environment (including the testing part).
Local Development Guide¶
So, you want to see some progress, preferably on Slack, and not just in the forms of unit testing? At this point, fear is actually a reasonable response. With this guide, you can be talking to your locally-hosted Slack bot in no time!
Warning: This only works smoothly with a Unix machine (macOS or Linux variants). Windows users may be in for more pain than expected.
1: Install ngrok¶
Slack requires that all webhooks are passed through HTTPS. This is rather inconvenient if you just want to test while running on your local computer. Luckily, we have ngrok, a forwarding service that hosts a public HTTPS URL that passes to your local computer. Sign up for ngrok and download it here.
After installing, run ngrok http 5000
to create an ngrok URL that will be
passed to your local port 5000. As long as you run Rocket on port 5000 (see
below), you can then access it through the HTTPS URL that ngrok gives you. Note
that it is very important to use the HTTPS URL, not the HTTP URL.
An alternative to ngrok
is ``localtunnel` <https://github.com/localtunnel/localtunnel>`_,
which lets you use the same subdomain every time.
$ lt --port 5000 --subdomain my-amazing-rocket2
your url is: https://my-amazing-rocket2.localtunnel.me
2: Create a Slack Workspace¶
For testing, itâs useful to have your own Slack workspace set up. If you do not already have one, go here to create one, and follow the steps to set it up.
3: Create a Slack App¶
Follow the link here to create a new Slack app - you can name it whatever you like - and install it to the appropriate workspace.
3.1: Add a Bot User¶
In âAdd features and functionalityâ, add a bot user. Since this is just for testing, you can name the bot user whatever you like.
3.2: Install Slack App¶
In âInstall your app to your workspace,â click the button to install to your workspace. This will take you to a permissions page for the workspace - make sure this is for the correct workspace, and allow the app to connect.
3.3: Determine Credentials¶
Make note of the appâs signing secret, found in Settings -> Basic Information -> App Credentials, and the bot user OAuth access token, found in Features -> OAuth & Permissions -> Tokens for Your Workspace. These will be needed for the configuration step later.
4: Gain Access to AWS¶
Rocket makes use of AWS DynamoDB as its database, and for testing you will want
to test on the ârealâ DynamoDB. If you do not already have access to DynamoDB,
you can use it as part of the free tier of AWS. Create an AWS account for
yourself, then go to the IAM service and create a new user. The user name
doesnât particularly matter (though rocket2-dev-$NAME
is recommended), but
make sure you check âprogrammatic access.â In permissions, go to âAttach
existing permissions directlyâ and add the following policies:
AmazonDynamoDBFullAccess
CloudWatchLogsFullAccess
As you may have noticed, we not only want to use DynamoDB, but also CloudWatch. We send our logs to CloudWatch for easier storage and querying.
Finally, copy the provided access key ID and secret access key after creating the new user.
Note: if you are in the brussel-sprouts
Github team, you should already have
AWS credentials. Just ask.
Alternatively, just set up DynamoDB locally (the Docker-based
setup is probably the easiest) and set AWS_LOCAL=True
.
5: Set Up Config¶
Our repo already contains sample-env
, the main environmental configuration
file for the entire app, as well as the credentials/
directory, where you
will put credential files like the Github app private key. The file is split
into section. There is a general section (which should be the top bit), a
section on everything slack related, a section on Github and Github apps,
and a section on AWS. Please read the section about the configuration
system.
5.1: Set up Github App and organization¶
Register Rocket 2 as a Github App under an appropriate testing organization (our team has one of these set up already). Make sure to install the Github App to the organization in addition to registering it.
Under âPrivate keysâ, click âGenerate a new private keyâ. This will generate
and allow you to download a new secret key for Rocket 2. Save this to the
credentials/
directory as github_signing_key.pem
- it should already be in
the PEM file format, bracketed by:
-----BEGIN RSA PRIVATE KEY-----
...
-----END RSA PRIVATE KEY-----
Authenticating Rocket 2 as a Github App and obtaining an access token for the Github API should be automated, once the signing key is available.
After doing this, remember to put your ngrok HTTPS URL with /webhook
appended
at the end, into the âWebhook URLâ box. After doing this, you must go to the
appâs âPermissions & Eventsâ tab and set the following as Read & Write:
- Organization members
After doing so, please check the checkboxes below:
- Membership
- Organization
- Team
- Team add
6: Build and Run Container¶
This section assumes you already have installed Docker. Assuming you are in the directory containing the Dockerfile, all you need to do to build and run is the following two commands:
docker build -t rocket2-dev-img .
docker run --rm -it \
--env-file .env \
-p 0.0.0.0:5000:5000 \
rocket2-dev-img
# optionally include `--network="host"` for local dynamoDB
Note that the options passed to -p
in docker run
tell Docker what port
to run Rocket on. 0.0.0.0
is the IP address (in this case, localhost),
the first 5000
is the port exposed inside the container, and the second
5000
is the port exposed outside the container. The port exposed outside
the container can be changed (for instance, if port 5000 is already
in use in your local development environment), but in that case ensure that
ngrok is running on the same port. The option `âenv-file`` <https://docs.docker.com/engine/reference/commandline/run/#set-environment-variablesâeâenvâenv-file>`_
lets you pass in your configuration options.
Also note that, for your convenience, we have provided two scripts,
scripts/docker_build.sh
and scripts/docker_run_local.sh
, that run these
exact commands.
6.1: [Optional] Running without Docker¶
We highly recommend building and running on Docker, but building every time
you make a tiny change can be inconvenient. If you would like to run without
building a new Docker image every time, you can do so with pipenv run launch
.
This is in fact the same command Docker runs, but if you run outside Docker,
you may run into errors due to unexpected changes in your local development
environment.
7: Configure Slack App Features¶
In addition to a bot user, there are a couple other features that need to be enabled in the Slack app once the local instance of Rocket is running.
7.1: Add Event Subscriptions¶
In âAdd features and functionalityâ, add event subscriptions. In particular,
under Request URL, submit the ngrok HTTPS URL with /slack/events
appended to
the end. Note that ngrok will generate a new HTTPS URL every time it runs, so
you will have to repeat this step every time you launch ngrok. You will then
have to enable workspace and/or bot events that we want Rocket to listen for,
like the team_join
workspace event - ask the team for the most up-to-date list
of these.
7.2: Add Slash Command¶
In âAdd features and functionalityâ, add a slash command. In particular, under
Request URL, submit the ngrok HTTPS URL with /slack/commands
appended to the
end. For the actual command, anything will work, though the final app will use
/rocket
. Make sure you tick the box marked âEscape channels, users, and links
sent to your appâ, or else none of the @ signs will work properly!
8: Testing¶
This is the final and most important part: testing if it actually works or not.
Go to your Slack workspace and add Rocket (or whatever you named your Slack bot)
to the channel if you have yet to do so (just type @<bot name>
and Slack will
ask if you want to invite the bot into the channel).
To test if Rocket is running, type the command:
/rocket user help
If you see a list of options, Rocket is working!
8.1: Setting Up Admin Permissions¶
We currently havenât finished the command to add a user to the database or make them an admin, so we have to do it manually.
First, determine your Slack ID by reading the logs. The logs are formatted like so:
{slackid_making_the_command}:{command_itself}
The Slack IDs of other users will appear when you type @
followed by whatever
the userâs handle is. Slack automatically converts that handle into an ID.
Then, you have an option of either using the AWS command-line interface or using the AWS web interface.
You should already have the command line interface installed via pipenv. If not,
run the command pipenv install --dev
. Note that to run commands, you will
either have to go into the pipenv environment (with pipenv shell
) or prefix
every command with pipenv run
. Here is the command to create a user with a:
# The following command is split into multiple lines because it is long. Make
# sure that the actal command isn't split into multiple lines because it may
# complicate things.
aws dynamodb put-item --table-name USERS_TABLE\
--item '{"slack_id":{"S": "UE7PAG75L"},
"permission_level":{"S": "admin"}}'\
--endpoint-url http://localhost:8000
Replace USERS_TABLE
with whatever name you set in config.toml
.
Alternatively, you can directly edit the DynamoDB table via the AWS web
interface. Go to the DynamoDB service in the AWS web interface and open
the appropriate table. Click on the Items tab and then on âCreate itemâ.
Make sure thereâs a column for slack_id
and permission_level
,
where slack_id
is a String
with the appropriate value and
permission_level
is a String
with the value admin
.
8.2: Viewing a User¶
/rocket user view
The output of this command should be a stylish table displaying your Slack id and permissions level.
Now, you can continue with whatever testing you originally wanted to do. Remember to rebulid your Docker image every time you make a change!
Scheduler Guide¶
So, you want to write a module and add it to the ever-growing list of modules that run periodically for rocket 2? Well, you have come to the right place.
A very good example module can be found in the
app/scheduler/modules/random_channel.py
source file. I recommend that you
read it before starting development (donât worry, itâs very short).
Structure¶
All scheduler modules are to be placed in the app/scheduler/modules/
directory. As Python source files, of course. These files should house the
module class. Every class must inherit ModuleBase
.
Since you inherit the ModuleBase
class, you must implement the following
methods:
get_job_args: A dictionary of job configuration arguments to be passed into the scheduler.
do_it: A function that actually does the thing you want to do every time the conditions you specified in the job configuration mentioned above.
Job arguments¶
As you can see from the example, the following job arguments are returned:
{'trigger': 'cron',
'day_of_week': 'sat',
'hour': 12,
'name': self.NAME}
Our trigger type is cron
, meaning that it is supposed to fire once every time
the rest of the arguments fit. day_of_week
means which day it is supposed to
fire. hour
means which hour on that day it is supposed to fire. And every job
has to have a name, which is specified in the name
argument. For a more
detailed look at the different types of arguments and different trigger types
that arenât discussed here, have a look at the APScheduler
documentation.
Firing it¶
The function do_it
is called whenever it is time to execute the job. You can
use it to periodically message people, periodically check statistics, poll
Github, you name it.
Adding your module to the scheduler¶
To actually have the scheduler execute and remember your module (and job), you
must add the job to the scheduler. This can be achieved by adding your module
into the scheduler via the function __add_job
within the function
__init_periodic_tasks
. You can see that we already have initialized our
beloved RandomChannelPromoter
in that function, so just follow along with
your own module.
And look! That wasnât all that bad now wasnât it??
Scripts for Development¶
There are a few scripts in the scripts/
directory that aid in the development
of this project.
build_check.sh¶
scripts/build_check.sh
This is just the list of commands run to check the code for violations of Python style. It also runs the tests, and is the script that is run in our Github CI. Make sure to run before submitting a pull request!
This script also checks to see if the user is running DynamoDB locally, and if so, would include tests for it; if not, the tests that use DynamoDB will be deselected.
See git hooks.
port_busy.py¶
pipenv run python scripts/port_busy.py 8000
This is to check if a port is busy on the machine you are running on.
Used in place of nmap
for automatically checking if the port used for local
instances of DynamoDB is in use.
- Exits with 0 if the port is in use.
- Exits with 1 if there is an issue connecting with the port you provided.
- Exits with 2 if the âportâ you provided couldnât be converted to an integer.
- Exits with 3 if you didnât provide exactly 1 argument.
- Exits with 4 if the port is not already in use.
update.sh¶
scripts/update.sh
This should be run whenever any change to Pipfile
or Pipfile.lock
occurs on
your local copy of a branch. It updates any changed dependencies into your
virtual environment. This is equivalent to the user running:
pipenv sync --dev
Which, coincidentally, require the same number of characters to be typed. The
script should ideally be run after any instance of git pull
.
See git hooks.
download_dynamodb_and_run.sh¶
scripts/download_dynamodb_and_run.sh
This script downloads a copy of the latest local version of DynamoDB and forks
the process. It also sets up the environment in which you should run it in using
scripts/setup_localaws.sh
.
Please do not use this script; it is meant to be run by Github CI. Unless you enjoy having to download and run multiple DynamoDB processes.
setup_localaws.sh¶
scripts/setup_localaws.sh
This script automatically sets up your environment to better benefit a local
instance of DynamoDB. Only should be run once by users (though running it
multiple times would not hurt too too much). It requires aws
to be installed
through pipenv
.
docker_build.sh¶
scripts/docker_build.sh
This script builds a docker image rocket2-dev-img
, according to the
Dockerfile
. Equivalent to:
docker build -t rocket2-dev-img .
Make sure you have docker installed on your system beforehand.
docker_run_local.sh¶
scripts/docker_run_local.sh
This script runs a local docker image on your system, port 5000. Equivalent to:
docker run --rm -it -p 0.0.0.0:5000:5000 rocket2-dev-img
Make sure you have already built a rocket2-dev-img
, or have run
scripts/docker_build.sh
before-hand. docker
must also be installed.
Makefile for Git Hooks¶
cd scripts
make
This script simply installs the pre-commit hooks and post-merge hooks.
build_check.sh
is copied to .git/hooks/pre-commit
, and update.sh
is copied
to .git/hooks/post-merge
.
After installation, every time you try to make a commit, all the tests will be
run automatically to ensure compliance. Every time you perform a pull
or
merge
or rebase
, pipenv
will try to sync all packages and dependencies.
Makefile for Documentation¶
make clean html
This script builds all documentation and places the html into _build/
directory. Should mostly be used to test your documentation locally. Should be
run within a pipenv shell
environment.
We use Python sphinx
to generate documentation from reStructuredText and
Markdown files in this project. To configure (and change versions for the
documentation), edit conf.py
. docs/index.rst
is the index for all documentation.
Database Reference¶
users
Table¶
The users
table stores all the users. With DynamoDB, we only need to specify a
fixed attribute to be the primary index. In this case, the userâs slack_id
is
the primary index. All other attributes are specified in the model/user.py
file, and are also listed here:
Attribute Name | Description |
---|---|
slack_id |
String ; The userâs slack id |
email |
String ; The userâs email address |
github |
String ; The userâs Github handler |
github_user_id |
String ; The userâs Github user ID |
major |
String ; The subject major the user is in |
position |
String ; The userâs position in Launch Pad |
bio |
String ; A short (auto)biography |
image_url |
String ; The userâs avatar image URL |
permission_level |
String ; The userâs permission level |
karma |
Integer ; The userâs karma points |
The userâs permission level is one of [member
, admin
, team_lead
].
teams
Table¶
The teams
table stores all teams where github_team_id
is the primary index.
All other attributes are specified in the model/team.py
file, and are also
listed here:
Attribute Name | Description |
---|---|
github_team_id |
String ; The teamâs Github ID |
github_team_name |
String ; The teamâs Github name |
display_name |
String ; The teamsâs display |
platform |
String ; The teamâs working platform |
team_leads |
String Set ; The teamâs set of team leadsâ Github IDs |
members |
String Set ; The teamâs set of membersâ Github IDs |
projects
Table¶
The projects
table stores all projects where project_id
is the primary
index. All other attributes are specified in the model/project.py
file, and
are also listed here:
Attribute Name | Description |
---|---|
project_id |
String ; The projectâs unique SHA1 ID, salted with a timestamp |
github_team_id |
String ; The teamâs Github ID associated with the project |
github_urls |
String Set ; A set of URLs pointing to project repositories |
display_name |
String ; A name for the project |
short_description |
String ; A short description that outlines the project |
long_description |
String ; A longer and more in-depth description |
tags |
String Set ; A set of tags taken from the Github repositories |
website_url |
String ; A URL to the projectâs website |
medium_url |
String ; A URL to the projectâs medium page |
appstore_url |
String ; A URL to the projectâs Apple Appstore page |
playstore_url |
String ; A URL to the projectâs Google Playstore page |
The Configuration System¶
We use environmental variables for all of our configuration-related things. A
sample .env
file (which is what pipenv
looks for when it tries to launch)
can be found at sample-env
. Here is how each variable works. Note: all
variables are strings
SLACK_SIGNING_SECRET¶
Signing secret of the slack app. Can be found in the basic information tab of your slack app (api.slack.com/apps).
SLACK_API_TOKEN¶
The slack api token of your slack bot. Can be found under OAuth & Permissions tab of your slack app (under the name âBot user OAuth access tokenâ).
SLACK_NOFICIATION_CHANNEL¶
Name of the channel you want to have our rocket 2 slack bot to make service notifications in.
SLACK_ANNOUNCEMENT_CHANNEL¶
Name of the channel you want to have our rocket 2 slack bot to make announcements in.
GITHUB_APP_ID¶
The id of your Github app (found under your Github organization settings -> Developer Settings -> Github Apps -> Edit).
GITHUB_ORG_NAME¶
The name of your Github organization (the string in the URL whenever you go to the organization.
GITHUB_WEBHOOK_ENDPT¶
The path Github posts webhooks to.
GITHUB_WEBHOOK_SECRET¶
A random string of characters you provide to Github to help further obfuscate and verify that the webhook is indeed coming from Github.
GITHUB_KEY¶
The Github app signing key (can be found under Github organization settings -> Developer Settings -> Github Apps -> Edit (at the bottom you generate and download the key)). Paste the contents of the file as a string. See deployment for troubleshooting.
AWS_ACCESS_KEYID¶
The AWS access key id.
AWS_SECRET_KEY¶
The AWS secret key.
AWS_*_TABLE¶
The names of the various tables (leave these as they are).
AWS_REGION¶
The region where the AWS instance is located (leave these as they are).
AWS_LOCAL¶
Point all AWS DynamoDB requests to http://localhost:8000
. Optional, and
defaults to False
.
Requirements¶
MVP¶
Our MVP is essentially feature-parity with the original Rocket. In particular, we should have:
- An extensible Unix-style command system
user
command (member info)team
command (team management)help
command- Permissions system
All of these should be connected to a database, likely a cloud database like DynamoDb or Firebase.
We have decided not to pursue a full plugin-oriented architecture, as this would severely complicate our work and most likely would not be used.
Stretch Goals¶
- Currently Rocket does most of the work of managing the Launch Pad Github organization. Replicating and extending this behaviour would be our first priority after completing the MVP.
- More ways to access Rocket-the-service would be nice. In particular, a command-line interface should be relatively easy to build. A web-based dashboard would be useful, but likely too far outside scope.
- A reminders command has been specifically requested by the co-presidents.
- The co-presidents also have other feature requests that will be added as Github issues.
Non-functional & Other Requirements¶
- Rocket 2.0 will be containerized via Docker.
- All code will follow the PEP8 style guide; this will be automated with pycodestyle.
- There should be automated tests for most behaviour, run with a CI system, and code coverage should be collected and uploaded to Codecov.io.
- The command system should be reasonably extensible.
Deployment¶
Deployment Process¶
The following should be read as more of a reference than a guide. To deploy Rocket 2, you must follow the steps as if you were building it for local use, except some tweaks in regards to where it goes and more tooling-specific details mentioned below.
Hosting¶
Rocket 2 is currently hosted by an AWS EC2 t2.micro instance. Since this is a single-threaded application with a single worker thread, there is not much of a reason to go for anything more. Note: Adding more worker threads may cause âminorâ issues such as the scheduler running more than once, weird exceptions, and may prevent the server from running in some cases, which is why increasing the number of worker threads beyond 1 is not recommended.
If need-be, Inertia can help provision an instance for you.
Should you wish to set up your own Rocket 2 instance for deployment, you should
first be able to set up a Rocket 2 instance for testing on a local computer
with ngrok
forwarding. If you have successfully set up an instance on a remote
computer, you may still want to have a look.
For those of you who donât want too much of a hassle, hosting via Heroku is also a valid option, as Heroku does continuous deployment without the need of setting up Inertia, and also has built-in SSL so you donât need to set anything up. Be wary, however, that Heroku is almost twice as expensive as an AWS EC2 t2.micro instance.
Do note that you must set the environmental variables in the provided settings
page if you are to host via Heroku. For details regarding how you would input
the GITHUB_KEY
, please see below.
SSL¶
Before deploying for the first time, you must set up SSL and configuration for
Nginx, which we are using as a proxy server. This can be done by running the
scripts/setup_deploy.sh
script. This runs the official
Letâs Encrypt container to request SSL certificates,
sets up a cronjob to periodically re-validate them, and copies
nginx.conf
to the correct location. Do note that the Letâs
Encrypt container needs to use port 443, so if you have another process or
container using that port, you will need to kill it before running the
set up script.
Inertia¶
For UBC Launch Pad, we continuously deploy off the ec2-release
branch on
Github using UBC Launch Padâs
Inertia. This will pull the repo when
changes are merged, rebuild the containers from docker-compose.yml
, and
redeploy.
When deploying with Inertia, make sure that you are using a stable version of Inertia.
Since we have changed from using .toml
configuration files to using
environmental variables for configuration, you must inject them using inertia
{some name} env set AWS_LOCAL False
and the like. If you already have all your
environmental variables set up in your .env
file, you can send the entire file
over with inertia {some name} send .env
.
The GITHUB_KEY
is merely the GPG private key used to sign Github API requests.
We simply shove the entire file into a string and use it in the environmental
variable. Do note that doing this on the command line is somewhat difficult
because inertia
would treat the dashes --
in the string as flags and get
confused. Another thing to watch out for is that the command line ignores the
new lines in the string. The current working method of doing this is to pass in
the entire string with a single quote (which means that every symbol is taken
literally), then for every dash in the string, we add a forward slash \
in
front. We then replace all new lines with the literal \n
.
Our configuration code replaces these instances of \-
and \n
with actual
dashes and new lines.
Note that these replacements are not necessary on Heroku and you can simply copy and paste the contents of the key file directly into the box provided.
If you are using the .env
file approach, you only need to replace the new
lines and not the dashes.
Docker Compose¶
Our main deployment configuration is contained in
docker-compose.yml
. We deploy an Nginx container
to serve as a proxy, as well as building and running a Rocket 2 container.
The Nginx proxy exposes ports 80 and 443, for HTTP/S, which must also be
accessible from the outside world. The Rocket 2 container exposes port 5000,
as Gunicorn is listening on this port; this should not be accessible to
the outside world.
Note that Docker Compose has a rather complex networking utility. In particular,
note that to access HTTP endpoints in other composed containers, you must
reference them by their service name in docker-compose.yml
, not via
localhost. This is already handled in nginx.conf
.
Other Build Tools¶
Github Actions CI¶
Github Actions CI is a continuous
integration service that is used to build and test software projects hosted on
Github. To configure Github CI, a file pythonpackage.yml
needs to be added
to .github/workflows/
. This YAML file will contain the commands for
the automated tests that needs to run.
Every time a branch gets pushed into github, Github CI starts a job. A job is where Github clones the GitHub repository into a new virtual environment to test the code.
Docker¶
Docker is a program that run software packages called containers. Every container is isolated from each other and is a bundle (also known as image) of their own tools, applications, libraries and configuration files. However, containers are able to also communicate with each other through channels, and all containers are run by a single OS kernel. We use Docker in Rocket2 to make deployment to the server easier.
Docker is composed of 3 parts: Container, Services, and Stack.
Dockerfile
defines the container. Inside Dockerfile
is the environment that
would be set up. Inside the container for Rocket2, we have a copy of our app,
and all the dependencies and the virtual environment installed.
docker-compose.yml
defines the services that allow multiple containers to run
together.
Docker is different than virtual machines because it can run multiple containers using only one kernel which makes it more lightweight.
Questions and Answers¶
What is the ``db`` module?
The database db
module consists of the facade
and the dynamodb
database
we are using.
What is the ``command`` module?
The command
module is where the slack commands get parsed and passed on to the
backend so models can be created and the database can be populated.
What is the ``model`` module?
The model
module is where models are constructed.
Currently we have Team
and User
models.
How do ``db``, ``command``, ``model`` modules interact with each other? First a command is input through slack. Then, the input will be parsed so a model can be populated. After the model gets populated, the model can then be added into the db. The db contains a separate table for each type of model.
User Command Reference¶
Commands that manipulate user data. Remember that parameters with whitespace must be enclosed in quotation marks.
Options¶
/rocket user {add, edit, view, help, delete}
Add¶
/rocket user add [-f|--force]
Add the current user into the database. This command by default does not
overwrite users that have already been entered into the database. By using the
-f
flag, you force rocket2
to overwrite the entry in the database, if any.
Edit¶
/rocket user edit [--name NAME] [--email EMAIL] [--pos POSITION]
[--github GITHUB_HANDLE] [--major MAJOR]
[--bio BIOGRAPHY]
[--permission {member,team_lead,admin}]
Allows user to edit their Launch Pad profile. Admins and team leads can edit
another userâs Launch Pad profile by using [--member SLACKID]
option.
SLACK_ID
is the @
-name, for easy slack autocomplete.
If a user edits their Github handle, rocket will also add the handle to Launch Padâs Github organization.
# Normal use
/rocket user edit --name "Steven Universe" --email "su@gmail.com"
# Admin/Team lead use
/rocket user edit --member @s_universe --name "Steven Universe"
Admins can easily promote other admins or team leads.
/rocket user edit --member @s_universe --permission admin
View¶
/rocket user view [SLACKID]
Display information about a user. SLACK_ID
is the @
-name, for easy slack
autocomplete. If SLACK_ID
is not specified, this command displays information
about the one who ran the command instead.
Delete (Admin only)¶
/rocket user delete MEMBER_ID
Permanently delete a memberâs Launch Pad Profile. Can only be used by admins.
MEMBER_ID
is the @
-name, for easy slack autocomplete.
Team Command Reference¶
Commands that manipulate team data. Remember that parameters with whitespace must be enclosed by quotation marks.
Options¶
/rocket team {list, view, help, create, edit, add, remove, lead, delete}
Create (Team Lead and Admin only)¶
/rocket team create GITHUB_TEAM_NAME [--name DISPLAY_NAME]
[--platform PLATFORM]
[--channel CHANNEL]
[--lead SLACK_ID]
Create a new team with a Github team name and optional display name. The user
who runs the command will be automatically added to team as Team Lead. If the
--lead
flag is used, user with SLACK_ID
will be added as Team Lead
instead. If the --channel
flag is used, all members in specified
channel will be added. âSLACK_IDâ is the @
-name, for easy slack autocomplete.
We use Github API to create the team on Github.
The Github team name cannot contain spaces.
/rocket team create "struddle-bouts" --name "Struddle Bouts" --channel @brussel_sprouts
Edit (Team Lead* and Admin only)¶
/rocket team edit GITHUB_TEAM_NAME [--name DISPLAY_NAME] [--platform PLATFORM]
Edit the properties of a specific team. Team Leads can only edit the teams that they are a part of, but admins can edit any teams.
Add (Team Lead* and Admin only)¶
/rocket team add GITHUB_TEAM_NAME SLACK_ID
Add a user to the team. Team Leads can only add users into teams that they are a
part of, but admins can add users to any team. SLACK_ID
is the @
-name, for
easy slack autocomplete.
Users will be added to the teams on Github as well.
/rocket team add struddle-bouts @s_universe
Remove (Team Lead* and Admin only)¶
/rocket team remove GITHUB_TEAM_NAME SLACK_ID
Remove a user from a team, removes them as Team Lead if they were one. Team
Leads can only remove users from teams that they are a part of, but admins can
remove users from any team. SLACK_ID
is the @
-name, for easy slack
autocomplete.
Users will be removed from the teams on Github as well.
Lead (Team Lead* and Admin only)¶
/rocket team lead GITHUB_TEAM_NAME SLACK_ID [--remove]
Adds a user as Team Lead, and adds them to team if not already added.
If --remove
flag is used, will remove user as Team Lead, but not from the team.
Team Leads can only promote/demote users in teams that they are part
of, but admins can promote/demote users in any team. âSLACK_IDâ is the
@
-name, for easy slack autocomplete.
Delete (Team Lead* and Admin only)¶
/rocket team delete GITHUB_TEAM_NAME
Permanently delete a team. Team Leads can only delete teams that they are a part of, but admins can delete any team.
Project Command Reference¶
Commands to do with projects. Remember that parameters with whitespace must be enclosed by quotation marks.
Options¶
/rocket project {list, view, help, create, unassign, edit, assign, delete}
Create (Team Lead and Admin only)¶
/rocket project create GH_REPO GITHUB_TEAM_NAME [--name DISPLAY_NAME]
Creates a new project from the given repo. Fails if the caller is not the team lead of the specified team or an admin.
Unassign (Team Lead and Admin only)¶
/rocket project unassign PROJECT_ID
Unassigns the given project. Fails if the caller is not the team lead of the team assigned to the project or if the caller is not an admin.
Assign (Team Lead and Admin only)¶
/rocket project assign PROJECT_ID GITHUB_TEAM_NAME [-f]
Assigns the project to the team. Fails if another team is assigned the project.
If -f
flag is given, can reassign even if
another team is already assigned the project.
Fails if the caller is not the team lead of the team to assign the project to
or if the caller is not an admin.
Delete (Team Lead and Admin only)¶
/rocket project delete PROJECT_ID [-f]
Delete the project from database. An error occurs if the project is currently assigned.
If -f
flag is given, can be deleted
even if a team is assigned.
Fails if the caller is not the team lead projectâs assigned team
or if the caller is not an admin.
Karma Command Reference¶
Command to giveth or taketh away a userâs karma
Options¶
For normal users¶
/rocket @user ++
/rocket karma view @user
For admin only¶
/rocket karma set @user {amount}
/rocket karma reset --all
Examples¶
# normal user
/rocket @coolkid1 ++ #adds 1 karma to coolkid1
/rocket karma view @coolkid1 #view how much karma coolkid1 has
# admin only
/rocket karma set @coolkid1 5 #sets coolkid's karma to 5
/rocket karma reset --all #resets all users karma to 1
Database¶
Database Facade¶
Database Facade.
-
class
db.facade.
DBFacade
(db)¶ A database facade that gives an overall API for any databases.
Currently, we plan on having DynamoDB, but other databases, such as MongoDB or Postgres are also being considered. Please use this class instead of
db/dynamodb.py
, because we might change the databases, but the facade would stay the same.-
__init__
(db)¶ Initialize facade using a given class.
Currently, we can only initialize with
db.dynamodb.DynamoDB
.Parameters: db ( DynamoDB
) â Database class for API calls
-
bulk_retrieve
(Model, ks)¶ Retrieve a list of models from the database.
Keys not found in the database will be skipped.
Parameters: - Model (
Type
[~T]) â the actual class you want to retrieve - ks (
List
[str
]) â retrieve based on this key (or ID)
Return type: List
[~T]Returns: a list of models
Model
- Model (
-
delete
(Model, k)¶ Remove an object from a table.
Parameters: - Model (
Type
[~T]) â table type to remove the object from - k (
str
) â ID or key of the object to remove (must be primary key)
- Model (
-
query
(Model, params=[])¶ Query a table using a list of parameters.
Returns a list of
Model
that have all of the attributes specified in the parameters. Every item in parameters is a tuple, where the first element is the user attribute, and the second is the value.Example:
ddb = DynamoDb(config) users = ddb.query(User, [('platform', 'slack')])
If you try to query a table without any parameters, the function will return all objects of that table.:
projects = ddb.query(Project)
Attributes that are sets (e.g.
team.member
,project.github_urls
) would be treated differently. This function would check to see if the entry contains a certain element. You can specify multiple elements, but they must be in different parameters (one element per tuple).:teams = ddb.query(Team, [('members', 'abc123'), ('members', '231abc')])
Parameters: - Model (
Type
[~T]) â type of list elements youâd want - params (
List
[Tuple
[str
,str
]]) â list of tuples to match
Return type: List
[~T]Returns: a list of
Model
that fit the query parameters- Model (
-
query_or
(Model, params=[])¶ Query a table using a list of parameters.
Returns a list of
Model
that have one of the attributes specified in the parameters. Some might say that this is a union of the parameters. Every item in parameters is a tuple, where the first element is the user attribute, and the second is the value.Example:
ddb = DynamoDb(config) users = ddb.query_or(User, [('platform', 'slack')])
If you try to query a table without any parameters, the function will return all objects of that table.:
projects = ddb.query_or(Project)
Attributes that are sets (e.g.
team.member
,project.github_urls
) would be treated differently. This function would check to see if the entry contains a certain element. You can specify multiple elements, but they must be in different parameters (one element per tuple).:teams = ddb.query_or(Team, [('members', 'abc123'), ('members', '231abc')])
The above would get you the teams that contain either member
abc123
or231abc
.Parameters: - Model (
Type
[~T]) â type of list elements youâd want - params (
List
[Tuple
[str
,str
]]) â list of tuples to match
Return type: List
[~T]Returns: a list of
Model
that fit the query parameters- Model (
-
retrieve
(Model, k)¶ Retrieve a model from the database.
Parameters: - Model (
Type
[~T]) â the actual class you want to retrieve - k (
str
) â retrieve based on this key (or ID)
Raise: LookupError if key is not found
Return type: ~T
Returns: a model
Model
if key is found- Model (
-
store
(obj)¶ Store object into the correct table.
Object can be of type
model.user.User
,model.team.Team
, ormodel.project.Project
.Parameters: obj (~T) â Object to store in database Return type: bool
Returns: True if object was stored, and false otherwise
-
DynamoDB¶
DynamoDB.
-
class
db.dynamodb.
DynamoDB
(config)¶ Handles calls to database through API.
Please do not use this class, and instead use
db.facade.DBFacade
. This class only works on DynamoDB, and should not be used outside of the facade class.-
class
Const
(config)¶ A bunch of static constants and functions.
-
__init__
(config)¶ Initialize the constants.
-
get_key
(table_name)¶ Get primary key of the table name.
Parameters: cls â the name of the table Raise: TypeError if table does not exist Return type: str
Returns: primary key of the table
-
get_set_attrs
(table_name)¶ Get class attributes that are sets.
Parameters: cls â the table name Raise: TypeError if table does not exist Return type: List
[str
]Returns: list of strings of set attributes
-
get_table_name
(cls)¶ Convert class into corresponding table name.
Parameters: cls ( Type
[~T]) â EitherUser
,Team
, orProject
Raise: TypeError if it is not either User, Team, or Project Return type: str
Returns: table name string
-
-
__init__
(config)¶ Initialize facade using DynamoDB settings.
To avoid local tests failure when the DynamoDb server is used, a testing environment variable is set. When testing environmental variable is true, the local dynamodb is run. When testing environmental variable is true, the server dynamodb is run.
boto3.resource() takes in a service_name, region_name, and endpoint_url (only for local dynamodb). service_name: The name of a service, âdynamodbâ in this case. region_name: The name of the region associated with the client. A list of different regions can be obtained online. endpoint_url: The complete URL to use for the constructed client.
-
bulk_retrieve
(Model, ks)¶ Retrieve a list of models from the database.
Keys not found in the database will be skipped.
Parameters: - Model (
Type
[~T]) â the actual class you want to retrieve - ks (
List
[str
]) â retrieve based on this key (or ID)
Return type: List
[~T]Returns: a list of models
Model
- Model (
-
check_valid_table
(table_name)¶ Check if table with
table_name
exists.Parameters: table_name ( str
) â table identifierReturn type: bool
Returns: boolean value, true if table exists, false otherwise
-
delete
(Model, k)¶ Remove an object from a table.
Parameters: - Model (
Type
[~T]) â table type to remove the object from - k (
str
) â ID or key of the object to remove (must be primary key)
- Model (
-
query
(Model, params=[])¶ Query a table using a list of parameters.
Returns a list of
Model
that have all of the attributes specified in the parameters. Every item in parameters is a tuple, where the first element is the user attribute, and the second is the value.Example:
ddb = DynamoDb(config) users = ddb.query(User, [('platform', 'slack')])
If you try to query a table without any parameters, the function will return all objects of that table.:
projects = ddb.query(Project)
Attributes that are sets (e.g.
team.member
,project.github_urls
) would be treated differently. This function would check to see if the entry contains a certain element. You can specify multiple elements, but they must be in different parameters (one element per tuple).:teams = ddb.query(Team, [('members', 'abc123'), ('members', '231abc')])
Parameters: - Model (
Type
[~T]) â type of list elements youâd want - params (
List
[Tuple
[str
,str
]]) â list of tuples to match
Return type: List
[~T]Returns: a list of
Model
that fit the query parameters- Model (
-
query_or
(Model, params=[])¶ Query a table using a list of parameters.
Returns a list of
Model
that have one of the attributes specified in the parameters. Some might say that this is a union of the parameters. Every item in parameters is a tuple, where the first element is the user attribute, and the second is the value.Example:
ddb = DynamoDb(config) users = ddb.query_or(User, [('platform', 'slack')])
If you try to query a table without any parameters, the function will return all objects of that table.:
projects = ddb.query_or(Project)
Attributes that are sets (e.g.
team.member
,project.github_urls
) would be treated differently. This function would check to see if the entry contains a certain element. You can specify multiple elements, but they must be in different parameters (one element per tuple).:teams = ddb.query_or(Team, [('members', 'abc123'), ('members', '231abc')])
The above would get you the teams that contain either member
abc123
or231abc
.Parameters: - Model (
Type
[~T]) â type of list elements youâd want - params (
List
[Tuple
[str
,str
]]) â list of tuples to match
Return type: List
[~T]Returns: a list of
Model
that fit the query parameters- Model (
-
retrieve
(Model, k)¶ Retrieve a model from the database.
Parameters: - Model (
Type
[~T]) â the actual class you want to retrieve - k (
str
) â retrieve based on this key (or ID)
Raise: LookupError if key is not found
Return type: ~T
Returns: a model
Model
if key is found- Model (
-
store
(obj)¶ Store object into the correct table.
Object can be of type
model.user.User
,model.team.Team
, ormodel.project.Project
.Parameters: obj (~T) â Object to store in database Return type: bool
Returns: True if object was stored, and false otherwise
-
class
Factories¶
Interface¶
Github¶
Utility classes for interacting with Github API via PyGithub.
-
class
interface.github.
DefaultGithubFactory
(app_id, private_key)¶ Default factory for creating interface to Github API.
-
__init__
(app_id, private_key)¶ Init factory.
Parameters: - app_id (
str
) â Github Apps ID - private_key (
str
) â Private key provided by Github Apps registration
- app_id (
-
create
()¶ Create instance of pygithub interface with Github Apps API token.
Return type: Github
-
-
class
interface.github.
GithubInterface
(github_factory, org)¶ Utility class for interacting with Github API.
-
__init__
(github_factory, org)¶ Initialize bot by creating Github object and get organization.
-
add_team_member
(username, team_id)¶ Add user with given username to team with id team_id.
-
get_team_member
(username, team_id)¶ Return a team member with a username of username.
Return type: NamedUser
-
has_team_member
(username, team_id)¶ Check if team with team_id contains user with username.
Return type: bool
-
list_team_members
(team_id)¶ Return a list of users in the team of id team_id.
Return type: List
[NamedUser
]
-
org_add_admin
(username)¶ Add member with given username as admin to organization.
-
org_add_member
(username)¶ Add/update to member with given username to organization.
If the user is already in the organization, donât do anything.
Return type: str
-
org_create_team
(name)¶ Create team with given name and add to organization.
Parameters: name ( str
) â name of teamReturn type: int
Returns: Github team ID
-
org_delete_team
(id)¶ Get team with given ID and delete it from organization.
-
org_edit_team
(key, name, description=None)¶ Get team with given ID and edit name and description.
Parameters: - key (
int
) â teamâs Github ID - name (
str
) â new team name - description (
Optional
[str
]) â new team description
- key (
-
org_get_team
(id)¶ Given Github team ID, return team from organization.
Return type: Team
-
org_has_member
(username)¶ Return true if user with username is member of organization.
Return type: bool
-
org_remove_member
(username)¶ Remove member with given username from organization.
-
remove_team_member
(username, team_id)¶ Remove user with given username from team with id team_id.
-
-
interface.github.
handle_github_error
(func)¶ Github error handler that updates Github App API token if necessary.
Slack¶
Models¶
User¶
Data model to represent an individual user.
-
class
app.model.user.
User
(slack_id)¶ Represent a user with related fields and methods.
-
__init__
(slack_id)¶ Initialize the user with a given Slack ID.
-
classmethod
from_dict
(d)¶ Convert dict response object to user model.
Parameters: d ( Dict
[str
,Any
]) â the dictionary representing a userReturn type: ~T Returns: returns converted user model.
-
get_attachment
()¶ Return slack-formatted attachment (dictionary) for user.
Return type: Dict
[str
,Any
]
-
classmethod
is_valid
(user)¶ Return true if this user has no missing required fields.
- Required fields for database to accept:
slack_id
permissions_level
Parameters: user (~T) â user to check Return type: bool
Returns: return true if this user has no missing required fields
-
classmethod
to_dict
(user)¶ Convert user object to dict object.
The difference with the in-built
self.__dict__
is that this is more compatible with storing into NoSQL databases like DynamoDB.Parameters: user (~T) â the user object Return type: Dict
[str
,Any
]Returns: the dictionary representing the user
-
Team¶
Represent a data model for a team.
-
class
app.model.team.
Team
(github_team_id, github_team_name, display_name)¶ Represent a team with related fields and methods.
-
__init__
(github_team_id, github_team_name, display_name)¶ Initialize the team.
Parameters are a valid Github team ID, team name and display name.
-
add_member
(github_user_id)¶ Add a new memberâs Github ID to the teamâs set of membersâ IDs.
-
add_team_lead
(github_user_id)¶ Add a userâs Github ID to the teamâs set of team lead IDs.
-
discard_member
(github_user_id)¶ Discard the member of the team with Github ID in the argument.
-
discard_team_lead
(github_user_id)¶ Remove a userâs Github ID to the teamâs set of team lead IDs.
-
classmethod
from_dict
(d)¶ Convert dict response object to team model.
Parameters: d ( Dict
[str
,Any
]) â the dictionary representing a teamReturn type: ~T Returns: returns converted team model.
-
get_attachment
()¶ Return slack-formatted attachment (dictionary) for team.
-
get_basic_attachment
()¶ Return basic slack-formatted attachment (dictionary) for team.
-
has_member
(github_user_id)¶ Identify if any member has the ID specified in the argument.
Return type: bool
-
has_team_lead
(github_user_id)¶ Identify if user with given ID is a team lead.
Return type: bool
-
is_team_lead
(github_user_id)¶ Identify if user with given ID is a team lead.
Return type: bool
-
classmethod
is_valid
(team)¶ Return true if this team has no missing required fields.
- Required fields for database to accept:
github_team_name
github_team_id
Parameters: team (~T) â team to check Return type: bool
Returns: returns true if this team has no missing required fields
-
classmethod
to_dict
(team)¶ Convert team object to dict object.
The difference with the in-built
self.__dict__
is that this is more compatible with storing into NoSQL databases like DynamoDB.Parameters: team (~T) â the team object Return type: Dict
[str
,Any
]Returns: the dictionary representing the team
-
Project¶
Represent a team project.
-
class
app.model.project.
Project
(github_team_id, github_urls)¶ Represent a team project with team ID and related fields and methods.
-
__init__
(github_team_id, github_urls)¶ Initialize the team project.
Project ID is a UUID generated by
uuid.uuid4()
.Parameters: - github_team_id (
str
) â the Github team ID associated with the project - github_urls (
List
[str
]) â a set/list of URLs pointing to repositories
- github_team_id (
-
classmethod
from_dict
(d)¶ Return a project from a dict object.
Parameters: d ( Dict
[str
,Any
]) â the dictionary (usually from DynamoDB)Return type: ~T Returns: a Project object
-
get_attachment
()¶ Return slack-formatted attachment (dictionary) for project.
Return type: Dict
[str
,Any
]
-
classmethod
is_valid
(p)¶ Return true if this project has no missing fields.
- Required fields for database to accept:
__project_id
__github_urls
Parameters: project â project to check Return type: bool
Returns: true if this project has no missing fields
-
classmethod
to_dict
(p)¶ Return a dict object representing a project.
The difference with the in-built
self.__dict__
is that this is more compatible with storing into NoSQL databases like DynamoDB.Parameters: p (~T) â the Project object Return type: Dict
[str
,Any
]Returns: a dictionary representing a project
-
Tests¶
Utilities¶
Some important (and often-used) utility functions.
-
tests.util.
create_test_admin
(slack_id)¶ Create a test admin user with slack id, and with all other attributes set.
Property Preset Slack ID slack_id
Bio I like puppies and kittens! Email admin@ubc.ca Name Iemann Atmin Github kibbles Image URL https://via.placeholder.com/150 Major Computer Science Permission Admin Position Adrenaline Junkie Parameters: slack_id ( str
) â The slack id stringReturn type: User
Returns: a filled-in user model (no empty strings)
-
tests.util.
create_test_project
(github_team_id, github_urls)¶ Create a test project with project ID, URLs, and all other attributes set.
Property Preset ID SHA1(github_urls[0], time.time())
Team ID github_team_id
Github URLs github_urls
Display Name Rocket2 Short Descrip. Slack bot, team management, and onboarding system for⊠Long Descrip. Slack bot, team management, and onboarding system for⊠Tags python, docker, pipenv, waterboarding Website https://github.com/ubclaunchpad/rocket2 Appstore URL ÂŻ\_(ă)_/ÂŻ Playstore URL ÂŻ\_(ă)_/ÂŻ Parameters: - github_team_id (
str
) â The Github team ID - github_urls (
List
[str
]) â The URLs to all connected projects
Return type: Returns: a filled-in project model (no empty strings)
- github_team_id (
-
tests.util.
create_test_team
(tid, team_name, display_name)¶ Create a test team with team name, and with all other attributes the same.
Property Preset Github tid
Name slug team_name
Display display_name
Platform slack Members [âabc_123â] Parameters: - tid (
str
) â The github ID associated with the team - team_name (
str
) â The github team name slug - display_name (
str
) â The github team name
Return type: Returns: a filled-in team model (no empty strings)
- tid (
Utilities¶
Utility class for formatting Slack Messages.
-
utils.slack_msg_fmt.
wrap_code_block
(str)¶ Format code block.
-
utils.slack_msg_fmt.
wrap_emph
(str)¶ Format emph.
-
utils.slack_msg_fmt.
wrap_quote
(str)¶ Format quote.
-
utils.slack_msg_fmt.
wrap_slack_code
(str)¶ Format code.
The following are a few functions to help in handling command.
-
utils.slack_parse.
check_permissions
(user, team)¶ Check if given user is admin or team lead.
If team is specified and user is not admin, check if user is team lead in team. If team is not specified, check if user is team lead.
Parameters: Return type: bool
Returns: true if user is admin or a team lead, false otherwise
-
utils.slack_parse.
escape_email
(email)¶ Convert a string with escaped emails to just the email.
Before:
<mailto:email@a.com|email@a.com>
After:
email@a.com
Parameters: email ( str
) â email to convertReturn type: str
Returns: unescaped email
-
utils.slack_parse.
escaped_id_to_id
(s)¶ Convert a string with escaped IDs to just the IDs.
Before:
/rocket user edit --member <@U1143214|su> --name "Steven Universe"
After:
/rocket user edit --member U1143214 --name "Steven Universe"
Parameters: s ( str
) â string to convertReturn type: str
Returns: string where all instances of escaped ID is replaced with IDs
-
utils.slack_parse.
ios_dash
(s)¶ Convert a string with a dash (â) to just double-hyphens (â).
Before:
/rocket user edit âname "Steven Universe"
After:
/rocket user edit --name "Steven Universe"
Parameters: s ( str
) â string to convertReturn type: str
Returns: string where all dashes are replaced with double-hyphens
-
utils.slack_parse.
is_slack_id
(id)¶ Check if id given is a valid slack id.
Parameters: id ( str
) â string of the object you want to checkReturn type: bool
Returns: true if object is a slack id, false otherwise
-
utils.slack_parse.
regularize_char
(c)¶ Convert any unicode quotation marks to ascii ones.
Leaves all other characters alone.
Parameters: c ( str
) â character to convertReturn type: str
Returns: ascii equivalent (only quotes are changed)
Webhooks¶
Github¶
Handle GitHub webhooks.
-
class
app.controller.webhook.github.core.
GitHubWebhookHandler
(db_facade, config)¶ Encapsulate the handlers for all GitHub webhook events.
-
__init__
(db_facade, config)¶ Give handlers access to the database.
-
handle
(request_body, xhub_signature, payload)¶ Verify and handle the webhook event.
Parameters: - request_body (
bytes
) â Byte string of the request body - xhub_signature (
str
) â Hashed signature to validate
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]Returns: appropriate ResponseTuple depending on the validity and type of webhook
- request_body (
-
verify_hash
(request_body, xhub_signature)¶ Verify if a webhook event comes from GitHub.
Parameters: - request_body (
bytes
) â Byte string of the request body - xhub_signature (
str
) â Hashed signature to validate
Returns: Return True if the signature is valid, False otherwise
- request_body (
-
Define the abstract base class for a GitHub event handler.
-
class
app.controller.webhook.github.events.base.
GitHubEventHandler
(db_facade)¶ Define the properties and methods needed for a GitHub event handler.
-
__init__
(db_facade)¶ Give handler access to the database facade.
-
handle
(payload)¶ Handle a GitHub event.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
supported_action_list
¶ Provide a list of all actions this handler can handle.
Return type: List
[str
]
-
Handle GitHub membership events.
-
class
app.controller.webhook.github.events.membership.
MembershipEventHandler
(db_facade)¶ Encapsulate the handler methods for GitHub membership events.
-
handle
(payload)¶ Handle the event where a user is added or removed from a team.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
mem_added
(github_id, selected_team, team_name, github_username)¶ Help membership function if payload action is added.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
mem_remove
(github_id, selected_team, team_name)¶ Help membership function if payload action is removal.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
supported_action_list
¶ Provide a list of all actions this handler can handle.
Return type: List
[str
]
-
Handle GitHub organization events.
-
class
app.controller.webhook.github.events.organization.
OrganizationEventHandler
(db_facade)¶ Encapsulate the handler methods for GitHub organization events.
-
handle
(payload)¶ Handle when a user is added, removed, or invited to an organization.
If the member is removed, they are removed as a user from rocketâs db if they have not been removed already.
If the member is added or invited, do nothing.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
handle_added
(github_username, organization)¶ Help organization function if payload action is added.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
handle_invited
(github_username, organization)¶ Help organization function if payload action is invited.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
handle_remove
(member_list, github_id, github_username)¶ Help organization function if payload action is remove.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
supported_action_list
¶ Provide a list of all actions this handler can handle.
Return type: List
[str
]
-
Handle GitHub team events.
-
class
app.controller.webhook.github.events.team.
TeamEventHandler
(db_facade)¶ Encapsulate the handler methods for GitHub team events.
-
handle
(payload)¶ Handle team events of the organization.
This event is fired when a team is created, deleted, edited, or added or removed from a repository.
If a team is created, add or overwrite a team in rocketâs db.
If a team is deleted, delete the team from rocketâs db if it exists.
If a team is edited, overwrite the teamâs fields or create the team if necessary.
If the team is added or removed from a repository, do nothing for now.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
supported_action_list
¶ Provide a list of all actions this handler can handle.
Return type: List
[str
]
-
team_added_to_repository
(github_id, github_team_name, payload)¶ Help team function if payload action is added_to_repository.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
team_created
(github_id, github_team_name, payload)¶ Help team function if payload action is created.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
team_deleted
(github_id, github_team_name, payload)¶ Help team function if payload action is deleted.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
team_edited
(github_id, github_team_name, payload)¶ Help team function if payload action is edited.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-
team_removed_from_repository
(github_id, github_team_name, payload)¶ Help team function if payload action is removed_from_repository.
Return type: Tuple
[Union
[Dict
[str
,List
[Dict
[str
,Any
]]],str
,Dict
[str
,Any
]],int
]
-