4
1
Fork 0

Ported code over from beep v1. Wrote API documentation. Changed some POST/PATCH handlers to respond an empty body instead of just parroting the supplied body

pull/24/head
UnicodingUnicorn 2019-01-27 09:53:34 +08:00
parent 853bbe3da9
commit e4ce8ee2d8
10 changed files with 1137 additions and 1 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM golang:1.11-rc-alpine as build
RUN apk add --no-cache git=2.18.1-r0
WORKDIR /src
COPY go.mod go.sum *.go ./
RUN CGO_ENABLED=0 go build -ldflags "-s -w"
FROM scratch
COPY --from=build /src/core /core
ENTRYPOINT ["/core"]

462
README.md
View File

@ -1,3 +1,463 @@
# backend-core
Beep backend handling core relational information that is not updated often.
Beep backend handling core relational information that is not updated often.
## Quickstart
```
cockroach start --insecure
echo "create database core;" | cockroach sql --insecure
migrate -database cockroach://root@localhost:26257/core?sslmode=disable -source file://migrations goto 1
go build && ./core
```
## API
Unless otherwise noted, bodies and responses are with ```Content-Type: application/json```.
| Contents |
| -------- |
| [Create User](#Create-User) |
| [Get Users by Phone](#Get-Users-by-Phone) |
| [Get User](#Get-User) |
| [Create Conversation](#Create-Conversation) |
| [Delete Conversation](#Delete-Conversation) |
| [Update Conversation](#Update-Conversation) |
| [Get Conversations](#Get-Conversations) |
| [Get Conversation](#Get-Conversation) |
| [Create Conversation Member](#Create-Conversation-Member) |
| [Get Conversation Members](#Get-Conversation-Members) |
| [Create Contact](#Create-Contact) |
| [Get Contacts](#Get-Contacts) |
---
### Create User
```
POST /user
```
Create a new user.
#### Body
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| first_name | String | First name of the added user. | ✓ |
| last_name | String | Last name of the added user. | ✓ |
| phone_number | String | Phone number of the added user. Shouldn't be needed but makes life easier. | X |
#### Success Response (200 OK)
Created user object.
```json
{
"id": "<id>",
"first_name": "<first_name>",
"last_name": "<last_name>",
"phone_number": "<phone_number>"
}
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 400 | Error parsing submitted body, or fields first_name or last_name have a length of 0. |
| 500 | Error occurred inserting entry into database. |
---
### Get Users by Phone
```
GET /user
```
Get user(s) associated with the supplied phone number.
#### Querystring
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| phone_number | String | Phone number to be queried. | ✓ |
#### Success Response (200 OK)
List of users.
```json
[
{
"id": "<id>",
"first_name": "<first_name>",
"last_name": "<last_name>"
},
...
]
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 400 | Supplied phone_number is absent/an invalid phone number. |
| 500 | Error occurred retrieving entries from database. |
---
### Get User
```
GET /user/:user
```
Get a specific user by ID.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
#### Success Response (200 OK)
User object.
```json
{
"id": "<id>",
"first_name": "<first_name>",
"last_name": "<last_name>",
"phone_number": "<phone_number>"
}
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 404 | User with supplied ID could not be found in database |
| 500 | Error occurred retrieving entries from database. |
---
### Create Conversation
```
POST /user/:user/conversation
```
Create a new conversation for a user.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
#### Body
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| title | String | Title of the conversation | X |
#### Success Response (200 OK)
Conversation object.
```json
{
"id": "<id>",
"title": "<title>"
}
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 400 | Error occurred parsing the supplied body. |
| 404 | User with supplied ID could not be found in database. |
| 500 | Error occurred inserting entries into the database. |
---
### Delete Conversation
```
DELETE /user/:user/conversation/:conversation
```
Delete the specified conversation.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
| conversation | String | Conversation's ID. | ✓ |
#### Success Response (200 OK)
Empty body.
#### Errors
| Code | Description |
| ---- | ----------- |
| 404 | User/Conversation with supplied ID could not be found in database. |
| 500 | Error occurred deleting entries from the database. |
---
### Update Conversation
```
PATCH /user/:user/conversation/:conversation
```
Update a conversation's details (mainly just title for now).
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
| conversation | String | Conversation's ID. | ✓ |
#### Body
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| title | String | New title of the conversation. | X |
#### Success Response (200 OK)
Empty Body. (TODO: Updated conversation)
#### Errors
| Code | Description |
| ---- | ----------- |
| 400 | Error occurred parsing the supplied body. |
| 404 | User/Conversation with supplied ID could not be found in database. |
| 500 | Error occurred updating entries in the database. |
---
### Get Conversations
```
GET /user/:user/conversation
```
Get the conversations of the specified user.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
#### Success Response (200 OK)
List of conversations.
```json
[
{
"id": "<id>",
"title": "<title>"
},
...
]
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 500 | Error occurred updating entries in the database. |
---
### Get Conversation
```
GET /user/:user/conversation/:conversation
```
Get a specific conversation of a specific user.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
| conversation | String | Conversation's ID. | ✓ |
#### Success Response (200 OK)
Conversation object.
```json
{
"id": "<id>",
"title": "<title>"
}
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 404 | User/Conversation with supplied ID could not be found in database. |
| 500 | Error occurred retrieving entries from the database. |
---
### Create Conversation Member
```
POST /user/:user/conversation/:conversation/member
```
Add a member to the specified conversation of the specified member.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
| conversation | String | Conversation's ID. | ✓ |
#### Body
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | String | ID of the user to be added. | ✓ |
#### Success Response (200 OK)
Empty body.
#### Errors
| Code | Description |
| ---- | ----------- |
| 400 | Error occurred parsing the supplied body/The length of the ID supplied in the body is less than 1. |
| 404 | User/Conversation with supplied ID could not be found in database. |
| 500 | Error occurred updating entries in the database. |
---
### Get Conversation Members
```
GET /user/:user/conversation/:conversation/member
```
Get the members of the specified conversation of the specified member.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
| conversation | String | Conversation's ID. | ✓ |
#### Success (200 OK)
List of user objects in conversation.
```json
[
{
"id": "<id>",
"first_name": "<first_name>",
"last_name": "<last_name>"
},
...
]
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 500 | Error occurred retrieving entries from the database. |
---
### Create Contact
```
POST /user/:user/contact
```
Add a new contact for the specified user.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
#### Body
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| id | String | New contact's ID. | ✓ |
#### Success Response (200 OK)
Empty body
#### Errors
| Code | Description |
| ---- | ----------- |
| 400 | Error occurred parsing the supplied body/The length of the ID supplied in the body is less than 1 or equal to the user's ID. |
| 500 | Error occurred updating entries in the database. |
---
### Get Contacts
```
GET /user/:user/contact
```
Get the contacts of the specified user.
#### URL Params
| Name | Type | Description | Required |
| ---- | ---- | ----------- | -------- |
| user | String | User's ID. | ✓ |
#### Success (200 OK)
List of user objects in user's contacts.
```json
[
{
"id": "<id>",
"first_name": "<first_name>",
"last_name": "<last_name>"
},
...
]
```
#### Errors
| Code | Description |
| ---- | ----------- |
| 500 | Error occurred retrieving entries from the database. |

9
go.mod Normal file
View File

@ -0,0 +1,9 @@
module backend/core
require (
github.com/golang/protobuf v1.1.0 // indirect
github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
github.com/ttacon/libphonenumber v1.0.0
)

10
go.sum Normal file
View File

@ -0,0 +1,10 @@
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d h1:of6+TpypLAaiv4JxgH5aplBZnt0b65B4v4c8q5oy+Sk=
github.com/julienschmidt/httprouter v0.0.0-20180715161854-348b672cd90d/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84 h1:it29sI2IM490luSc3RAhp5WuCYnc6RtbfLVAB7nmC5M=
github.com/lib/pq v0.0.0-20180523175426-90697d60dd84/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0=
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w=
github.com/ttacon/libphonenumber v1.0.0 h1:5DJsnAoMCC+LjJ6lQqjjf2EHiDD6KH4rVhDsMn5oXII=
github.com/ttacon/libphonenumber v1.0.0/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M=

506
handlers.go Normal file
View File

@ -0,0 +1,506 @@
package main
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)
type Handler struct {
db *sql.DB
}
func (h *Handler) CreateUser(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// Parse
user := User{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&user)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Validate
phone, err := ParsePhone(user.PhoneNumber)
if err != nil || len(user.FirstName) < 1 || len(user.LastName) < 1 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
user.PhoneNumber = phone // shouldn't be needed but makes life easier
// Generate ID
id := "u-" + RandomHex()
user.ID = id
// Log
log.Print(user)
// Insert
_, err = h.db.Exec(`
INSERT INTO "user" (id, first_name, last_name, phone_number) VALUES ($1, $2, $3, $4)
`, user.ID, user.FirstName, user.LastName, user.PhoneNumber)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Respond
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (h *Handler) GetUsersByPhone(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
// Parse
phone, err := ParsePhone(r.FormValue("phone_number"))
// Validate
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Response object
users := make([]User, 0)
// Select
rows, err := h.db.Query(`
SELECT id, first_name, last_name FROM "user" WHERE phone_number = $1
`, phone)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
defer rows.Close()
// Scan
for rows.Next() {
user := User{}
if err := rows.Scan(&user.ID, &user.FirstName, &user.LastName); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
users = append(users, user)
}
// Respond
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
// Response object
user := User{}
// Select
err := h.db.QueryRow(`
SELECT id, first_name, last_name, phone_number FROM "user" WHERE id = $1
`, userID).Scan(&user.ID, &user.FirstName, &user.LastName, &user.PhoneNumber)
switch {
case err == sql.ErrNoRows:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
case err != nil:
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Respond
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func (h *Handler) CreateConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
conversation := Conversation{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&conversation)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Generate ID
id := "c-" + RandomHex()
conversation.ID = id
// Log
log.Print(conversation)
// Insert
tx, err := h.db.Begin()
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Conversation
_, err1 := tx.Exec(`
INSERT INTO "conversation" (id, title) VALUES ($1, $2)
`, conversation.ID, conversation.Title)
// First member
_, err2 := tx.Exec(`
INSERT INTO member ("user", "conversation") VALUES ($1, $2)
`, userID, conversation.ID)
if err1 != nil || err2 != nil {
// likely 404...
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
log.Print(err1, err2)
return
}
err = tx.Commit()
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Respond
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(conversation)
}
func (h *Handler) GetConversations(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
// Response object
conversations := make([]Conversation, 0)
// Select
rows, err := h.db.Query(`
SELECT id, title FROM "conversation"
INNER JOIN member
ON member.conversation = "conversation".id AND member.user = $1
`, userID)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
defer rows.Close()
// Scan
for rows.Next() {
var id, title string
if err := rows.Scan(&id, &title); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
conversations = append(conversations, Conversation{id, title})
}
// Respond
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(conversations)
}
func (h *Handler) GetConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
conversationID := p.ByName("conversation")
// Response object
conversation := Conversation{}
// Select
err := h.db.QueryRow(`
SELECT id, title FROM "conversation"
INNER JOIN member
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
`, userID, conversationID).Scan(&conversation.ID, &conversation.Title)
switch {
case err == sql.ErrNoRows:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
case err != nil:
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Respond
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(conversation)
}
func (h *Handler) UpdateConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
conversationID := p.ByName("conversation")
conversation := Conversation{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&conversation)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Check
var conversationID2 string
err = h.db.QueryRow(`
SELECT id FROM "conversation"
INNER JOIN member
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
`, userID, conversationID).Scan(&conversationID2)
switch {
case err == sql.ErrNoRows:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
case err != nil:
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Update
if len(conversation.Title) > 0 {
_, err = h.db.Exec(`
UPDATE "conversation"
SET title = $2
WHERE id = $1
`, conversationID, conversation.Title)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
}
w.WriteHeader(200);
}
func (h *Handler) DeleteConversation(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
userID := p.ByName("user")
conversationID := p.ByName("conversation")
// Delete
tx, err := h.db.Begin()
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Check
var conversationID2 string
err = h.db.QueryRow(`
SELECT id FROM "conversation"
INNER JOIN member
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
`, userID, conversationID).Scan(&conversationID2)
switch {
case err == sql.ErrNoRows:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
case err != nil:
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Users in Conversation
_, err1 := tx.Exec(`
DELETE FROM "member" WHERE "conversation" = $1
`, conversationID)
// Conversation
_, err2 := tx.Exec(`
DELETE FROM "conversation" WHERE "id" = $1
`, conversationID)
if err1 != nil || err2 != nil {
// likely 404...
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
log.Print(err1, err2)
return
}
err = tx.Commit()
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
w.WriteHeader(200)
}
func (h *Handler) CreateConversationMember(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
conversationID := p.ByName("conversation")
member := User{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&member)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Validate
if len(member.ID) < 1 {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Log
log.Print(member)
// Check
var conversationID2 string
err = h.db.QueryRow(`
SELECT id FROM "conversation"
INNER JOIN member
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
`, userID, conversationID).Scan(&conversationID2)
switch {
case err == sql.ErrNoRows:
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
case err != nil:
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Insert
_, err = h.db.Exec(`
INSERT INTO member ("user", "conversation") VALUES ($2, $1)
`, conversationID, member.ID)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Respond
//w.Header().Set("Content-Type", "application/json")
//json.NewEncoder(w).Encode(member)
w.WriteHeader(200)
}
func (h *Handler) GetConversationMembers(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
conversationID := p.ByName("conversation")
// Response object
users := make([]User, 0)
// Select
rows, err := h.db.Query(`
SELECT "user".id, "user".first_name, "user".last_name FROM "user"
INNER JOIN member m ON "user".id = m.user
INNER JOIN conversation ON "conversation".id = m.conversation
INNER JOIN member
ON member.conversation = "conversation".id AND member.user = $1 AND member.conversation = $2
`, userID, conversationID)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
defer rows.Close()
// Scan
for rows.Next() {
var id, firstName, lastName string
if err := rows.Scan(&id, &firstName, &lastName); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
users = append(users, User{ID: id, FirstName: firstName, LastName: lastName})
}
// Respond
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func (h *Handler) CreateContact(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
contact := User{}
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&contact)
if err != nil {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Validate
if len(contact.ID) < 1 || contact.ID == userID {
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
return
}
// Insert
_, err = h.db.Exec(`
INSERT INTO contact ("user", contact) VALUES ($1, $2)
`, userID, contact.ID)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
// Respond
//w.Header().Set("Content-Type", "application/json")
//json.NewEncoder(w).Encode(contact)
}
func (h *Handler) GetContacts(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
// Parse
userID := p.ByName("user")
// Response object
contacts := make([]User, 0)
// Select
rows, err := h.db.Query(`
SELECT id, first_name, last_name, phone_number FROM "user"
INNER JOIN contact
ON contact.contact = "user".id AND contact.user = $1
`, userID)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
defer rows.Close()
// Scan
for rows.Next() {
var id, firstName, lastName, phone string
if err := rows.Scan(&id, &firstName, &lastName, &phone); err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
log.Print(err)
return
}
contacts = append(contacts, User{id, firstName, lastName, phone})
}
// Respond
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(contacts)
}
func NewHandler(db *sql.DB) *Handler {
return &Handler{db}
}

63
main.go Normal file
View File

@ -0,0 +1,63 @@
package main
import (
"database/sql"
"flag"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
_ "github.com/lib/pq"
)
var listen string
var postgres string
func main() {
// Parse flags
flag.StringVar(&listen, "listen", ":8080", "host and port to listen on")
flag.StringVar(&postgres, "postgres", "postgresql://root@localhost:26257/core?sslmode=disable", "postgres string")
flag.Parse()
// Open postgres
log.Printf("connecting to postgres %s", postgres)
db, err := sql.Open("postgres", postgres)
if err != nil {
log.Fatal(err)
}
defer db.Close()
// Handler
h := NewHandler(db)
// Routes
router := httprouter.New()
// Users
router.POST("/user/", h.CreateUser)
router.GET("/user/", h.GetUsersByPhone)
router.GET("/user/:user", h.GetUser)
//router.PATCH("/user/:user", h.UpdateUser)
// Conversations
router.POST("/user/:user/conversation/", h.CreateConversation)
router.GET("/user/:user/conversation/", h.GetConversations) // USER MEMBER CONVERSATION
router.DELETE("/user/:user/conversation/:conversation", h.DeleteConversation)
//router.GET("/user/:user/conversation/bymembers/", h.GetConversationsByMembers) // TODO
router.GET("/user/:user/conversation/:conversation", h.GetConversation) // USER MEMBER CONVERSATION
router.PATCH("/user/:user/conversation/:conversation", h.UpdateConversation) // USER MEMBER CONVERSATION ADMIN=true -> update conversation title
//router.DELETE("/user/:user/conversation/:conversation", h.DeleteConversation) // USER MEMBER CONVERSATION -> delete membership
router.POST("/user/:user/conversation/:conversation/member/", h.CreateConversationMember) // USER MEMBER CONVERSATION ADMIN=true -> create new membership
router.GET("/user/:user/conversation/:conversation/member/", h.GetConversationMembers) // USER MEMBER CONVERSATION
//router.DELETE("/user/:user/conversation/:conversation/member/:member", h.DeleteConversationMember) // USER MEMBER CONVERSATION ADMIN=true -> delete membership
// Last heard
//router.GET("/user/:user/lastheard/:conversation", h.GetLastheard)
//router.PUT("/user/:user/lastheard/:conversation", h.SetLastheard)
// Contacts
router.POST("/user/:user/contact/", h.CreateContact)
router.GET("/user/:user/contact/", h.GetContacts)
//router.GET("/user/:user/contact/:contact", h.GetContact)
//router.DELETE("/user/:user/contact/:contact", h.DeleteContact)
//router.GET("/user/:user/contact/:contact/conversation/", h.GetContactConversations)
log.Printf("starting server on %s", listen)
log.Fatal(http.ListenAndServe(listen, router))
}

View File

@ -0,0 +1,25 @@
CREATE TABLE "user" (
id BYTEA PRIMARY KEY,
first_name VARCHAR(65535),
last_name VARCHAR(65535),
phone_number VARCHAR(32) UNIQUE
);
CREATE TABLE "conversation" (
id BYTEA PRIMARY KEY,
title VARCHAR(65535)
);
CREATE TABLE member (
"user" BYTEA REFERENCES "user"(id),
"conversation" BYTEA REFERENCES "conversation"(id),
UNIQUE ("user", "conversation")
);
CREATE TABLE contact (
"user" BYTEA REFERENCES "user"(id),
contact BYTEA REFERENCES "user"(id),
UNIQUE ("user", contact)
);

13
types.go Normal file
View File

@ -0,0 +1,13 @@
package main
type Conversation struct {
ID string `json:"id"` // id
Title string `json:"title"` // title
}
type User struct {
ID string `json:"id"` // id
FirstName string `json:"first_name"` // first_name
LastName string `json:"last_name"` // last_name
PhoneNumber string `json:"phone_number"` // phone_number
}

25
utils.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"crypto/rand"
"encoding/hex"
"github.com/ttacon/libphonenumber"
)
func RandomHex() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
panic("unable to generate 16 bytes of randomness")
}
return hex.EncodeToString(b)
}
func ParsePhone(phone string) (string, error) {
num, err := libphonenumber.Parse(phone, "")
if err != nil {
return "", err
}
return libphonenumber.Format(num, libphonenumber.INTERNATIONAL), nil
}