🚀 Rocket 2¶

codecov Deployed with Inertia Documentation Status

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.

GITHUB_KEY¶

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.

Help¶
/rocket user help

Display options for the user commands.

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}
List¶
/rocket team list

Display a list of Github team names and display names of all teams.

View¶
/rocket team view GITHUB_TEAM_NAME

Display information and members of a specific team.

Help¶
/rocket team help

Display options for team commands.

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}
List¶
/rocket project list

Display a list of all projects.

View¶
/rocket project view PROJECT_ID

Displays details of project.

Help¶
/rocket project help

Displays options for project command.

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.

Edit¶
/rocket project edit PROJECT_ID [--name DISPLAY_NAME]

Edit the given project.

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¶
Add 1 karma to user¶
/rocket @user ++
View a user’s karma¶
/rocket karma view @user
For admin only¶
Set user karma¶
/rocket karma set @user {amount}
Reset all user karma¶
/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
Help¶
Display options for karma commands¶
/rocket karma help

Commands¶

Commands Core¶
User¶
Team¶

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

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)
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

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 or 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

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

store(obj)¶

Store object into the correct table.

Object can be of type model.user.User, model.team.Team, or model.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]) – Either User, Team, or Project
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

check_valid_table(table_name)¶

Check if table with table_name exists.

Parameters:table_name (str) – table identifier
Return 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)
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

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 or 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

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

store(obj)¶

Store object into the correct table.

Object can be of type model.user.User, model.team.Team, or model.project.Project.

Parameters:obj (~T) – Object to store in database
Return type:bool
Returns:True if object was stored, and false otherwise

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
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 team
Return 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
org_get_team(id)¶

Given Github team ID, return team from organization.

Return type:Team
org_get_teams()¶

Return array of teams associated with organization.

Return type:List[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 user
Return 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 team
Return 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
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
Permissions¶

Data model to represent permissions.

class app.model.permissions.Permissions¶

Enum to represent possible permissions levels.

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 string
Return 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:

Project

Returns:

a filled-in project model (no empty strings)

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:

Team

Returns:

a filled-in team model (no empty strings)

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:
  • user (User) – user who’s permission needs to be checked
  • team (Optional[Team]) – team you want to check that has user as team lead
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 convert
Return 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 convert
Return 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 convert
Return 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 check
Return 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 convert
Return 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

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

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]
Slack¶

Indices and tables¶