Skip to content
This repository has been archived by the owner on Oct 21, 2019. It is now read-only.

Commit

Permalink
Update docs, minor refactor to API to hold organization as field (#83)
Browse files Browse the repository at this point in the history
Update docs, minor refactor to API to hold organization as field
  • Loading branch information
bobheadxi authored and bfbachmann committed Jun 22, 2018
1 parent 089187f commit 9b6e70e
Show file tree
Hide file tree
Showing 18 changed files with 153 additions and 73 deletions.
54 changes: 26 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

Rocket is the management and onboarding system for UBC Launch Pad. More information can be found in the [Wiki](https://github.com/ubclaunchpad/rocket/wiki). Rocket is a Slack bot you can talk to at ubclaunchpad.slack.com by messaging `@rocket`. It features GitHub integration, a robust command framework, and a simple interface through which plugins can easily be added.

- [Development](#development)
- [Creating Your Own Rocket Plugin](#creating-your-own-rocket-plugin)
- [Architecture](#architecture)
- [Slack Bot](#slack-bot)
- [Server](#server)
- [Database](#database)
- [Deployment](#deployment)

<br>

## Development

To get started, make sure you have [Golang](https://golang.org/doc/install#install) installed and download the Rocket codebase:
Expand Down Expand Up @@ -30,17 +40,9 @@ func TestMyIntegratedFunction(t *testing.T) {
}
```

## Architecture

### Rocket

The [Bot](bot/bot.go) holds references to structures that we use to communicate with our external dependencies (Slack, GitHub, and Postgres). It also contains logic for handling Slack messages. The `commands` property maps from command name to command handler.

[server.go](server/server.go) defines some handlers for HTTP requests. Our website will make requests to `/api/teams` and `/api/members` to display information about our teams and members. Note that content is served over HTTPS using `acme/autocert` to get TLS certificates from LetsEncrypt.

#### Plugins
### Creating Your Own Rocket Plugin

A plugin is intended to be a standalone component of Rocket. A Rocket Plugin is simply any type that implements the [Plugin](plugin/plugin.go) interface:
Features can easily be added to Rocket through Rocket's plugin framework. A Rocket Plugin is simply any type that implements the [Plugin](plugin/plugin.go) interface:

```go
// Plugin is any type that exposes Slack commands and event handlers, and can
Expand Down Expand Up @@ -75,43 +77,39 @@ func (wp *Plugin) EventHandlers() map[string]bot.EventHandler {

You can use the `Start` method of your plugin to start any background tasks you need to. Any `Commands` and `EventHandlers` you expose to Rocket in your implementation of the Plugin interface will be automatically registered with the `Bot`. See the Slack's [API Event Types](https://api.slack.com/events) for a list of events and their names if you implement your own `EventHandler`s for your plugin.

When creating a new plugin, make a new package for your plugin at the same level as the `core` package (within the `plugins` directory), create your type that implements the `Plugin` interface, and register your plugin in [plugin.RegisterPlugins](plugin/plugin.go). It is recommended that you place any commands you write for your plugin in their own separate files under your plugin's package.
To add your plugin to Rocket, just make a new package for your plugin at the same level as the `core` package (within the `plugins` directory), create your type that implements the `Plugin` interface, and register your plugin in [plugin.RegisterPlugins](plugin/plugin.go). Once you are done, open up a pull request! :tada:

## Architecture

### Slack Bot

The [Bot](bot/bot.go) holds references to structures that we use to communicate with our external dependencies (Slack, GitHub, and Postgres). It also contains logic for handling Slack messages. The `commands` property maps from command name to command handler.

#### Commands

The command framework can be found in `cmd`. It defines a set of data structures and functions for parsing, validating, and automatically documenting Rocket commands. All commands are defined in the `bot` package.

New commands should go in their own files in the `bot` package. When creating a new command you must define the following properties:

* `Name`: The command name. Rocket will use this to assign a Slack message to a specific command handler in [bot/bot.go:handleMessageEvent](bot/bot.go).
* `HelpText`: A description of what the command does. You don't need to describe the options here as you'll do that in the `HelpText` field of the `Option` struct.
* `Options`: A mapping of option key to option. The key for a given option in the `Options` map should always match the `key` field in that option.
* `HandleFunc`: The `CommandHandler` that executes the command. It should take `cmd.Context` as it's only argument and return a `string` response message with `slack.PostMessageParameters`.
#### Plugins

#### Options
A Rocket plugin is intended to be a standalone component of Rocket. Rocket's core Slack functionality is implemented as a plugin in [package core](plugins/core).

When specifying an option for a command you'll need to fill in the following fields:
### Server

* `Key`: The key that identifies this option. Of course, keys for different options under the same command should always be unique. For exmaple, one might create a command with one option who's key is `name`. In this case the user would assign a value to this key in their Slack command with `name={myvalue}`.
* `HelpText`: A description of what the option is used for.
* `Format`: A `regexp.Regexp` object that specifies the required format of a value for an option. The `cmd` framework will enforce that this format is met when a user enters a value for a given option, and will return an appropriate error response if this is not the case. Commonly used format `Regex`s can be found in [bot/util.go](bot/util.go).
* `Required`: Whether or not a value for this option is required when a user uses this command. The `cmd` framework will enforce that a value is set for each required option when a user enters a command, and will return an appropriate error if this is not the case.
[server.go](server/server.go) defines some handlers for HTTP requests. Our website will make requests to `/api/teams` and `/api/members` to display information about our teams and members. Note that content is served over HTTPS using `acme/autocert` to get TLS certificates from LetsEncrypt.

#### Querying the DB
### Database

We use the [go-pg](https://github.com/go-pg/pg) for querying our Postgres database from Rocket. The `dal` package provides an interface to querying our database. The `model` package holds all our data structures that are used by the `dal` package in our queries.

The database schema is defined in [tables.go](schema/tables.sql).

## Docker Setup
## Deployment

_This section is for reference or for when moving Rocket to a new server. On the current Google Cloud server, Docker is already setup._

We use [Docker](https://docs.docker.com/install/) and [docker-compose](https://docs.docker.com/compose/install/) to run Rocket and the Postgres database that it relies on. In order for Rocket to access the database the rocket container (called "rocket" in `docker-compose.yml`) needs to be running on the same Docker network as the Postgres container (called "postgres" in `docker-compose.yml`). Starting both containers with `docker-compose up` will create a Docker container network called `rocket_default`. Once this is done Rocket will be able to access the DB with the host name `postgres`.

### Deployment

Before deploying you will have to create two config files using the templates provided in `.app.env.example` and `.db.env.exmaple`. Copy these files and add the relevant values to them. Here are the recommended settings with passwords an security tokens omitted:
Before deploying, you will have to create two config files using the templates provided in `.app.env.example` and `.db.env.exmaple`. Copy these files and add the relevant values to them. Here are the recommended settings with passwords an security tokens omitted:

#### App Environment Variables

Expand Down
2 changes: 2 additions & 0 deletions bot/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package bot contains Rocket's Slack bot class
package bot
52 changes: 17 additions & 35 deletions cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,27 @@ import (
"strings"

"github.com/nlopes/slack"
"github.com/ubclaunchpad/rocket/model"
)

// Command represents a command that Rocket will recognise and respond to.
type Command struct {
Name string
HelpText string
Options map[string]*Option
// Name identifies this Command. Rocket will use this to assign a Slack
// message to a specific command handler in [bot/bot.go:handleMessageEvent](bot/bot.go)
Name string

// HelpText is a description of what the command does. You don't need to
// describe the options here as you'll do that in the `HelpText` field of
// the `Option` struct.
HelpText string

// Options is a mapping of option keys to their corresponding option struct.
// The key for a given option in the `Options` map should always match
// the `key` field in that option.
Options map[string]*Option

// HandleFunc is the `CommandHandler` that executes the command. It should
// take `cmd.Context` as its only argument and return a `string` response
// message with `slack.PostMessageParameters`.
HandleFunc CommandHandler
}

Expand Down Expand Up @@ -117,34 +130,3 @@ func (c *Command) parseOptions(opts []string) error {
}
return nil
}

// Option represents a parameter that can be passed as part of a
// Rocket command
type Option struct {
Key string
HelpText string
Format *regexp.Regexp
Required bool
Value string
}

// validate returns nil if the given value meets the format requirements for
// this option, returns the validation error otherwise.
func (o *Option) validate(value string) error {
// Check that the value meets the required format
if !o.Format.MatchString(value) {
return fmt.Errorf("Invalid format for option \"%s\". "+
"Format must match regular expression %s.", o.Key, o.Format.String())
}
return nil
}

// Context stores a Slack message and the user who sent it.
type Context struct {
Message *slack.Msg
User model.Member
Options map[string]Option
}

// CommandHandler is the interface all handlers of Rocket commands must implement.
type CommandHandler func(Context) (string, slack.PostMessageParameters)
2 changes: 2 additions & 0 deletions cmd/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package cmd contains Rocket's Slack command framework
package cmd
16 changes: 16 additions & 0 deletions cmd/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cmd

import (
"github.com/nlopes/slack"
"github.com/ubclaunchpad/rocket/model"
)

// Context stores a Slack message and the user who sent it.
type Context struct {
Message *slack.Msg
User model.Member
Options map[string]Option
}

// CommandHandler is the interface all handlers of Rocket commands must implement.
type CommandHandler func(Context) (string, slack.PostMessageParameters)
45 changes: 45 additions & 0 deletions cmd/option.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package cmd

import (
"fmt"
"regexp"
)

// Option represents a parameter that can be passed as part of a
// Rocket command
type Option struct {
// Key is this option's identifier. Of course, keys for different options
// under the same command should always be unique. For exmaple, one might
// create a command with one option who's key is `name`. In this case the
// user would assign a value to this key in their Slack command with
// `name={myvalue}`.
Key string
Value string

// HelpText is a description of what the option is used for.
HelpText string

// Format is a `regexp.Regexp` object that specifies the required format of
// a value for an option. The `cmd` framework will enforce that this format
// is met when a user enters a value for a given option, and will return an
// appropriate error response if this is not the case. Commonly used format
// `Regex`s can be found in [bot/util.go](bot/util.go).
Format *regexp.Regexp

// Required defines whether or not a value for this option is required when
// a user uses this command. The `cmd` framework will enforce that a value
// is set for each required option when a user enters a command, and will
// return an appropriate error if this is not the case.
Required bool
}

// validate returns nil if the given value meets the format requirements for
// this option, returns the validation error otherwise.
func (o *Option) validate(value string) error {
// Check that the value meets the required format
if !o.Format.MatchString(value) {
return fmt.Errorf("Invalid format for option \"%s\". "+
"Format must match regular expression %s.", o.Key, o.Format.String())
}
return nil
}
2 changes: 2 additions & 0 deletions config/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package config contains Rocket's configuration setup and structs
package config
2 changes: 2 additions & 0 deletions data/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package data provides Rocket's interface to its database
package data
9 changes: 9 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
Rocket is the management and onboarding system for UBC Launch Pad.
It is a Slack bot that features GitHub integration, a robust command
framework, and a simple interface through which plugins can easily be added.
*/
package main
2 changes: 2 additions & 0 deletions github/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package github contains Rocket's interface to GitHub's API.
package github
23 changes: 16 additions & 7 deletions github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import (

// API provides a client to the GitHub API.
type API struct {
httpClient *http.Client
organization string
httpClient *http.Client
*gh.Client
}

// New creates and returns an API object based on a configuration object.
func New(c *config.Config) *API {
// New creates and returns a GitHub API object based on a configuration object,
// configured for use with the given organization
func New(organization string, c *config.Config) *API {
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: c.GithubToken},
Expand All @@ -28,11 +30,13 @@ func New(c *config.Config) *API {
client := gh.NewClient(tc)

return &API{
organization,
tc,
client,
}
}

// UserExists checks if a given user exists in Github
func (api *API) UserExists(username string) (bool, error) {
_, _, err := api.Users.Get(context.Background(), username)
if err != nil {
Expand All @@ -41,29 +45,33 @@ func (api *API) UserExists(username string) (bool, error) {
return true, nil
}

// AddUserToTeam adds given user to given team
func (api *API) AddUserToTeam(username string, teamID int) error {
_, _, err := api.Organizations.AddTeamMembership(
context.Background(), teamID, username, nil,
)
return err
}

// RemoveUserFromOrg removes given user from configured organization
func (api *API) RemoveUserFromOrg(username string) error {
_, err := api.Organizations.RemoveOrgMembership(
context.Background(), username, "ubclaunchpad",
context.Background(), username, api.organization,
)
return err
}

// RemoveUserFromTeam removes given user from configured organization
func (api *API) RemoveUserFromTeam(username string, teamID int) error {
_, err := api.Organizations.RemoveTeamMembership(
context.Background(), teamID, username,
)
return err
}

// CreateTeam creates a team in the configured organization
func (api *API) CreateTeam(name string) (*gh.Team, error) {
teams, _, err := api.Organizations.ListTeams(context.Background(), "ubclaunchpad", nil)
teams, _, err := api.Organizations.ListTeams(context.Background(), api.organization, nil)
if err != nil {
return nil, err
}
Expand All @@ -80,12 +88,13 @@ func (api *API) CreateTeam(name string) (*gh.Team, error) {
Name: name,
Privacy: gh.String("closed"),
}
t, _, err := api.Organizations.CreateTeam(context.Background(), "ubclaunchpad", team)
t, _, err := api.Organizations.CreateTeam(context.Background(), api.organization, team)
return t, err
}

// GetTeam retrieves team with given team ID from configured organization
func (api *API) GetTeam(id int) (*gh.Team, error) {
teams, _, err := api.Organizations.ListTeams(context.Background(), "ubclaunchpad", nil)
teams, _, err := api.Organizations.ListTeams(context.Background(), api.organization, nil)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func main() {
}()

// Create a client to the GitHub API, using the token from the config.
gh := github.New(cfg)
gh := github.New("ubclaunchpad", cfg)

// Set up a server listening on the interface specified in the
// config. This will panic if the server fails to bind to the interface
Expand Down
2 changes: 2 additions & 0 deletions model/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package model contains the structs that define members and teams.
package model
3 changes: 3 additions & 0 deletions plugin/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package plugin contains the Plugin interface through which new functionality
// is added to Rocket.
package plugin
2 changes: 2 additions & 0 deletions plugins/core/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package core contains Rocket's core functionality in the form of a plugin.
package core
3 changes: 3 additions & 0 deletions plugins/welcome/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package welcome contains the Welcome plugin that welcomes new users to the
// Slack workspace
package welcome
3 changes: 3 additions & 0 deletions server/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Package server implements a read-only REST API interface to Rocket's team
// management data.
package server
2 changes: 0 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Package server implements a read-only REST API interface to Rocket's team
// management data.
package server

import (
Expand Down

0 comments on commit 9b6e70e

Please sign in to comment.