Compare commits

...

42 Commits

Author SHA1 Message Date
Chakib Benziane 23f2e99f4a docker data submodule 5 years ago
Chakib Benziane bd9a980133 admin token view 5 years ago
Chakib Benziane 9ecdef717a beta warning 5 years ago
Chakib Benziane aa90856f59 fix db 5 years ago
Chakib Benziane 44de8bc448 Working download !!! 5 years ago
Chakib Benziane 4985b3aea3 update caddyfile 5 years ago
Chakib Benziane fdd586d11c Wip download invoice status 5 years ago
Chakib Benziane d8c8c6a49b download check invoice 5 years ago
Chakib Benziane 4105c33e25 WIP download start and keep dl invoice session 5 years ago
Chakib Benziane 53f37ff619 Better session handling, wip download invoice
- Improved API endpoints logic
- Use gorilla sessions for more granularity
5 years ago
Chakib Benziane 24fec00864 fix api handling 5 years ago
Chakib Benziane 4d764d057c fix prod caddy 5 years ago
sp4ke cefd7ba558 Merge branch 'master' of ssh://git.sp4ke.com:10024/sp4ke/bit4sat 5 years ago
sp4ke 8f845374dd update prod options 5 years ago
Chakib Benziane 5ff79cbec3 Fix cookie credentials bug, add prod mode 5 years ago
Chakib Benziane e97dc51f8d show download link and admin token 5 years ago
Chakib Benziane 0f5075acab db refactoring + wip dl id 5 years ago
Chakib Benziane a2ae0ce517 home and uplaod view with pay management/wip admin token 5 years ago
Chakib Benziane 6b74f86164 routes + Base style scaffolding 5 years ago
Chakib Benziane 314e3b7624 Session fixes, working upload scenario 5 years ago
Chakib Benziane fd7cec6600 Working lnd grpc with invoice polling 5 years ago
Chakib Benziane 23abc0b369 lnd wip invoice watcher 5 years ago
Chakib Benziane ea20251d07 use lnd grpc (later sphinx), wip polling 5 years ago
Chakib Benziane 0d4a3d00e6 show download link 5 years ago
Chakib Benziane be1f3ae1b7 Fixed timer 5 years ago
Chakib Benziane 849d5ebff5 mvp paid upload (missing download link) 5 years ago
Chakib Benziane 93a0260ecc Front working upload/pay scenario 5 years ago
Chakib Benziane 89fc1b2894 Using callback no need for watchers 5 years ago
Chakib Benziane e4ca0e2701 Working invoice payment 5 years ago
Chakib Benziane c1900d52a3 implemented invoice watcher 5 years ago
Chakib Benziane bcd3160cfd websocket ready, wip invoice watcher 5 years ago
Chakib Benziane b1e4ee4196 Wip websocket + use caddy + fix CSP and CORS 5 years ago
Chakib Benziane f22a691152 working invoice qrcode 5 years ago
Chakib Benziane 38e55de611 Using postgresql 5 years ago
Chakib Benziane 10949a9ba3 wip postgres 5 years ago
Chakib Benziane ae30315d16 Receive LN invoice on upload 5 years ago
Chakib Benziane bb90656197 Working upload process + redis cache 5 years ago
Chakib Benziane f3299ad19c wip convert upload_id to RawBytes 5 years ago
Chakib Benziane 634e94909c working mariadb access + redis 5 years ago
Chakib Benziane 6627ffcee7 maraidb compose file 5 years ago
Chakib Benziane f1a01ebe47 fix compose 5 years ago
Chakib Benziane b31df12a3d use docker, adding redis 5 years ago

@ -0,0 +1,5 @@
node_modules
**/node_modules
dist
bit4sat
file-storage

3
.gitmodules vendored

@ -0,0 +1,3 @@
[submodule "docker"]
path = docker
url = git@git.sp4ke.com:sp4ke/bit4sat-docker

@ -1,11 +0,0 @@
FROM golang:alpine
RUN mkdir -p /src
WORKDIR /src
ENV GO111MODULE=on
COPY . .
RUN go build
CMD ["bit4sat"]

@ -2,6 +2,8 @@
TARGET := bit4sat
GOINSTALL := GO111MODULE=on go install -v
GOBUILD := GO111MODULE=on go build -v
WEBDIST := bit4sat/webdist
WEBDIST_DOCKER := docker/Dockerfile-dist-prod
.PHONY: all build install
@ -12,3 +14,6 @@ build:
install:
$(GOINSTALL)
webdist:
docker build -t $(WEBDIST) -f $(WEBDIST_DOCKER) .

@ -1,37 +0,0 @@
package main
import (
"git.sp4ke.com/sp4ke/bit4sat-server.git/storage"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
var (
UploadCtrl = storage.UploadCtrl{}
)
type API struct {
router *gin.Engine
}
func (api *API) Run() {
uploadRoute := api.router.Group("/upload")
{
uploadRoute.POST("/", UploadCtrl.New)
uploadRoute.PUT("/:id", UploadCtrl.Upload)
}
api.router.Run(":8880")
}
func NewAPI() *API {
router := gin.Default()
router.Use(cors.Default())
return &API{
router: router,
}
}

@ -0,0 +1,34 @@
package api
import (
"net/http"
"git.sp4ke.com/sp4ke/bit4sat/storage"
"git.sp4ke.com/sp4ke/bit4sat/utils"
"github.com/gin-gonic/gin"
)
func adminGetInfo(c *gin.Context) {
adminToken := c.Param("adminToken")
// Get upload payments
//upInfo, err := storage.GetUploadInfoByToken(adminToken)
//if err != nil {
//utils.JSONErrPriv(c, http.StatusInternalServerError, err)
//return
//}
// Sum of payments
paySum, err := storage.GetMsatBalanceByAdminToken(adminToken)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{
"msat_avail": paySum,
})
return
}

@ -0,0 +1,327 @@
package api
import (
"encoding/gob"
"fmt"
"log"
"net/http"
"time"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/ln"
"git.sp4ke.com/sp4ke/bit4sat/storage"
"git.sp4ke.com/sp4ke/bit4sat/utils"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/mediocregopher/radix/v3"
)
func download(c *gin.Context) {
var ok bool
dlId := c.Param("dlId")
// Get upload id for dl id
upId, err := storage.GetUploadIdForDlId(dlId)
if err != nil {
utils.JSONErr(c, http.StatusNotFound, "this file does not exist anymore")
return
}
// Get files metadata for upload
var upFilesMeta []storage.FileUpload
upFilesMeta, err = storage.GetUploadFilesMeta(upId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Get the download session
session, err := SessionStore.Get(c.Request, DlSessionKey)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
dlSessVal := session.Values["session-id"]
var dlSess string
if dlSess, ok = dlSessVal.(string); dlSessVal != nil && !ok {
log.Printf("error parsing session dlSessVal")
utils.JSONErr(c, http.StatusExpectationFailed,
"start a new download session session")
return
}
dlIdVal := session.Values["dlId"]
var sessDlId string
// If no dlId was stored before than this is a new download
if dlIdVal == nil {
sessDlId = dlId
} else {
if sessDlId, ok = dlIdVal.(string); !ok {
log.Printf("cannot cast sessDlId value")
utils.JSONErr(c, http.StatusExpectationFailed,
"start a new download session session")
return
}
}
// Test if we are alread in a download session
// If the stored dlId is different than the
// current dl id it means we're downloading a different
// file
if dlSessVal == nil || (sessDlId != dlId) {
// This is a new download session
log.Println("new download session")
session.Values["dlId"] = dlId
// Create a unique session id for this download session
sessId, err := storage.GetShortId()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
log.Printf("going to generate invoice for %s", upId)
//TODO: use fee asked by uploader
//
// Get upload data asked by uploader
up, err := storage.GetUploadById(upId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
invoiceOpts := ln.InvoiceOpts{
Amount: up.AskAmount,
Curr: ln.CurrencyID[up.AskCurrency],
Memo: fmt.Sprintf("bit4sat download: %s", sessId),
}
log.Printf("invoice opts %#v", invoiceOpts)
invoice, err := ln.NewInvoice(invoiceOpts)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Store invoice_id ---> upload_id
key := fmt.Sprintf("invoice_id_%s_upload_id", invoice.RHash)
err = db.DB.Redis.Do(radix.FlatCmd(nil, "SET", key, upId))
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Store invoice_<invoice_id>
invoiceKey := fmt.Sprintf("invoice_%s", invoice.RHash)
err = db.DB.Redis.Do(radix.FlatCmd(nil, "SET", invoiceKey, invoice))
session.Values["session-id"] = sessId
session.Values["invoice-id"] = invoice.RHash
invoiceExpires := time.Time(invoice.ExpiresAt).Sub(time.Now()).Seconds()
//TODO: this session can be reused during this hour
session.Options.MaxAge = int(invoiceExpires)
//log.Printf("session: %#v", session.Values)
err = session.Save(c.Request, c.Writer)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusPaymentRequired, gin.H{
"invoice": invoice,
"files": upFilesMeta,
})
// This is a returning download session to pay the invoice
} else {
log.Printf("continue download session id: %s", dlSess)
// Get invoice-id linked to sessionId
invIdVal := session.Values["invoice-id"]
if invIdVal == nil {
session.Options.MaxAge = -1
err = session.Save(c.Request, c.Writer)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
utils.JSONErr(c, http.StatusExpectationFailed,
"invoice not found, start a new download session session")
return
}
var invoiceId string
if invoiceId, ok = invIdVal.(string); !ok {
log.Printf("Could not find invoice in session %s", dlSess)
session.Options.MaxAge = -1
err = session.Save(c.Request, c.Writer)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
utils.JSONErr(c, http.StatusExpectationFailed,
"invoice not found, start a new download session session")
return
}
// Get invoice from invoice_id
invoiceKey := fmt.Sprintf("invoice_%s", invoiceId)
invoice := ln.Invoice{}
err = db.DB.Redis.Do(radix.FlatCmd(&invoice, "GET", invoiceKey))
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// If invoice paid create an authorized download link linked to session
// The link will expire after after one hour
// Also bump up the expirey of the DlSession to one hour
if invoice.Settled {
log.Println("dl invoice paid, sending")
downKey, err := storage.GetShortId()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
key := fmt.Sprintf("session_%s_download_key_%s", dlSess, downKey)
// We store the upload id in the value to use it when retreiving
// files
db.SetKeyVal(key, upId)
db.ExpireKey(key, MaxAgeDlKeySession)
// Session will also expire in 1 hour
session.Options.MaxAge = MaxAgeDlKeySession
err = session.Save(c.Request, c.Writer)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{
"download_key": downKey,
"invoice": invoice,
})
return
}
c.JSON(http.StatusPaymentRequired, gin.H{
"invoice": invoice,
"files": upFilesMeta,
})
}
return
}
func getFiles(c *gin.Context) {
downKey := c.Param("dlKey")
// Get the session
session, err := SessionStore.Get(c.Request, DlSessionKey)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
dlSessVal := session.Values["session-id"]
var dlSess string
var ok bool
if dlSess, ok = dlSessVal.(string); dlSessVal != nil && !ok {
log.Printf("error parsing session dlSessVal")
utils.JSONErr(c, http.StatusExpectationFailed,
"user session lost, start a new download session session")
return
}
if dlSessVal == nil {
utils.JSONErr(c, http.StatusGone, "the download session has expired")
return
}
key := fmt.Sprintf("session_%s_download_key_%s", dlSess, downKey)
// Check if download key exists in db
exists, err := db.Exists(key)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if !exists {
utils.JSONErr(c, http.StatusGone, "the download session has expired")
return
}
// We should now allow the user to get the files
// We retreive the files from the upId stored on the session download key
var upId string
err = db.GetFromKey(key, &upId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Get files metadata for upload
var upFilesMeta []storage.FileUpload
upFilesMeta, err = storage.GetUploadFilesMeta(upId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
c.Header("Content-Type", "application/zip")
c.Header("Content-Disposition", "attachment; filename=download.zip")
err = storage.ZipFiles(upFilesMeta, c.Writer)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
return
}
func TestDownHandler(c *gin.Context) {
sess := sessions.Default(c)
test := sess.Get("test")
log.Printf("%#v", test)
if test != nil {
sess.Clear()
sess.Save()
c.String(http.StatusOK, "I remember you")
} else {
sess.Set("test", 1)
sess.Save()
c.String(http.StatusOK, "i dont remember you")
}
return
}
func init() {
gob.Register(&ln.Invoice{})
gob.Register(&[]storage.FileUpload{})
}

@ -0,0 +1,182 @@
package api
import (
"fmt"
"log"
"net/http"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/ln"
"git.sp4ke.com/sp4ke/bit4sat/storage"
"git.sp4ke.com/sp4ke/bit4sat/utils"
"github.com/gin-gonic/gin"
"github.com/mediocregopher/radix/v3"
)
func upSessionHandler(c *gin.Context) {
upSession, err := SessionStore.Get(c.Request, UpSessionKey)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
lastUp, exists := upSession.Values["lastUp"]
if exists {
err = upSession.Save(c.Request, c.Writer)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Get upload status
uId := lastUp.([]string)[0]
key := fmt.Sprintf("upload_status_%s", uId)
var upStatus storage.UpStatus
err := db.DB.Redis.Do(radix.FlatCmd(&upStatus, "GET", key))
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError,
fmt.Errorf("error finding upload status %s", lastUp))
fmt.Println(err)
return
}
invoice, err := storage.GetUploadInvoice(lastUp.(string))
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// if invoice is not empty
c.JSON(http.StatusOK, gin.H{
"uploadId": lastUp,
"status": upStatus,
"invoice": invoice,
})
return
}
c.JSON(http.StatusOK, gin.H{
"uploadId": 0,
})
return
}
func pollInvoice(c *gin.Context) {
invoiceId := c.Param("rhash")
// First check if already paid
// Get the invoice from invoice_id
invoiceKey := fmt.Sprintf("invoice_%s", invoiceId)
invoice := ln.Invoice{}
err := db.DB.Redis.Do(radix.FlatCmd(&invoice, "GET", invoiceKey))
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// If paid return ok
if invoice.Settled {
c.JSON(http.StatusOK, gin.H{
"invoice": invoice,
})
// Poll
} else {
invoiceChan := make(chan *ln.Invoice)
errorChan := make(chan error)
log.Printf("starting invoice poll for %s", invoice.RHash)
// If waiting payment, wait until invoice is paid
go ln.PollPaidInvoice(invoice.RHash, invoiceChan, errorChan)
select {
case invoice := <-invoiceChan:
// if expired return with expired response
if invoice.Expired {
c.JSON(http.StatusGone, gin.H{
"invoice": invoice,
})
return
}
////////////////
///// Invoice was paid
////////////////
//
log.Println("POLL: invoice was paid")
c.JSON(http.StatusOK, gin.H{
"invoice": invoice,
})
return
case err := <-errorChan:
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// We stopped waiting for the invoice, it must have expired
c.JSON(http.StatusGone, gin.H{
"invoice": invoice,
})
return
}
}
// Was used by ln-charge
func invoiceCbHandler(c *gin.Context) {
invoice := ln.Invoice{}
//dec := json.Decoder(c.Request.Body)
if err := c.ShouldBindJSON(&invoice); err != nil {
log.Println(err)
utils.JSONErr(c, http.StatusNotAcceptable,
"callback: could not parse request from charge api")
return
}
log.Printf("received invoice paid from ln charge\n%s", invoice)
c.Status(http.StatusOK)
// get upload id related to invoice
var uploadId string
invoiceUploadKey := fmt.Sprintf("invoice_%s_upload_id", invoice.RHash)
err := db.DB.Redis.Do(radix.FlatCmd(&uploadId, "GET", invoiceUploadKey))
if err != nil {
panic(err)
}
if uploadId == "" {
log.Printf("cannot find uploadId for invoice %s", invoice)
return
}
// Invoice paid !!!!
// Set upload status to paid
err = storage.SetUploadStatus(uploadId, storage.UpPaid)
if err != nil {
panic(err)
}
//
// Update Upload Invoice
err = storage.SetUploadInvoice(uploadId, &invoice)
if err != nil {
panic(err)
}
return
}

@ -0,0 +1,88 @@
package api
import (
"git.sp4ke.com/sp4ke/bit4sat/config"
"github.com/boj/redistore"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
var (
upCtrl = UploadCtrl{}
)
type API struct {
router *gin.Engine
}
func (api *API) Run() {
// Get last session if it exists
v1 := api.router.Group("/api/v1")
v1.GET("/session", upSessionHandler)
uploadRoute := v1.Group("/u")
{
uploadRoute.POST("", upCtrl.New)
uploadRoute.PUT("/:id", upCtrl.Upload)
//TODO: make a poll version + this one
uploadRoute.GET("/check/:id", upCtrl.CheckStatus)
uploadRoute.GET("/poll/:id", upCtrl.PollStatus)
}
// Invoice poller
// invoice id is the RHash field
v1.GET("/pollinvoice/:rhash", pollInvoice)
// Download route
downRoute := v1.Group("/d")
{
// Query download
downRoute.GET("/q/:dlId", download)
// Get file
downRoute.GET("/g/:dlKey", getFiles)
}
adminRoute := v1.Group("/a")
{
adminRoute.GET("/info/:adminToken", adminGetInfo)
}
// test rout
v1.GET("/t", TestDownHandler)
// Websocket server
//api.router.GET("/ws", ws.Serve)
// LN charge callback
//api.router.POST("/"+config.ChargeCallbackEndpoint, invoiceCbHandler)
api.router.Run(config.ApiListen)
}
func NewAPI() *API {
router := gin.Default()
router.Use(cors.Default())
// Setup Session
var err error
SessionStore, err = redistore.NewRediStore(10, "tcp", "redis:6379",
"", []byte(config.SessionSecret))
if err != nil {
panic(err)
}
//
//
//router.Use(secure.New(secure.Config{
//ContentSecurityPolicy: "default-src 'self'; script-src *; worker-src *",
////IsDevelopment: true,
//}))
return &API{
router: router,
}
}

@ -0,0 +1,13 @@
package api
import "github.com/boj/redistore"
const (
UpSessionKey = "bit4sat-up"
DlSessionKey = "bit4sat-dl"
MaxAgeDlKeySession = 3600 // max age for a paid and authorized download key
)
var (
SessionStore *redistore.RediStore
)

@ -0,0 +1,522 @@
package api
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"os"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/ln"
"git.sp4ke.com/sp4ke/bit4sat/storage"
"git.sp4ke.com/sp4ke/bit4sat/utils"
"github.com/gin-gonic/gin"
)
type UploadCtrl struct{}
//TODO: tell client to avoid sending duplicate if we already have hash
func (ctrl UploadCtrl) New(c *gin.Context) {
uploadForm := UploadForm{}
if err := c.ShouldBindJSON(&uploadForm); err != nil {
log.Printf("error parsing form: %s", err)
utils.JSONErr(c, http.StatusNotAcceptable,
"invalid form")
return
}
// Create unique id
uploadId, err := storage.GetShortId()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
tx, err := db.DB.Sql.Beginx()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if uploadForm.RequestPayment {
if _, ok := ln.CurrencyID[uploadForm.PaymentCurrency]; !ok {
utils.JSONErr(c, http.StatusInternalServerError,
"currency not handled or recognized")
return
}
}
// Create new upload
up := &storage.Upload{}
up.UploadId = uploadId
up.AskFee = uploadForm.RequestPayment
up.AskCurrency = uploadForm.PaymentCurrency
up.AskAmount = uploadForm.RequestPaymentAmount
// Generate unique download id
//
dlId, err := storage.GetShortId()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
up.DownloadId = dlId
err = up.TxWrite(tx)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
db.RollbackTx(tx, nil)
return
}
for _, file := range uploadForm.Files {
fup := &storage.FileUpload{}
fup.UploadId = uploadId
fup.FileName, fup.FileExt = utils.CleanFileName(file.Name)
fup.FileSize = file.Size
fup.FileType = file.Type
fup.SHA256 = file.SHA256
err := fup.TxWrite(tx)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
db.RollbackTx(tx, nil)
return
}
}
err = tx.Commit()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Set upload status to new
err = storage.SetUploadStatus(uploadId, storage.UpNew)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Upload seems valid, handle new upload procedure
//
// Get the upload session
upSession, err := SessionStore.Get(c.Request, UpSessionKey)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Set the uploadId as last upload for session
upSession.AddFlash(uploadId)
err = upSession.Save(c.Request, c.Writer)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// If not free upload generate a an invoice
// TODO: check if upload fee is free
if uploadForm.RequestPayment {
invoiceOpts := ln.InvoiceOpts{
Amount: uploadForm.RequestPaymentAmount,
Curr: ln.CurrencyID[uploadForm.PaymentCurrency],
Memo: fmt.Sprintf("bit4sat upload: %s", uploadId),
}
invoice, err := ln.NewInvoice(invoiceOpts)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Update Upload Invoice
err = storage.SetUploadInvoice(uploadId, invoice)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Start watching upload payment status for this client
//err = WatchUploadPayment(invoice)
//if err != nil {
//log.Println("error watching payment ", err)
//utils.JSONErrPriv(c, http.StatusInternalServerError, err)
//return
//}
c.JSON(http.StatusOK, gin.H{
"status": storage.UpNew,
"invoice": invoice,
"upload_id": uploadId,
})
return
// Handle free uploads
} else {
log.Println("new upload created")
c.JSON(http.StatusOK, gin.H{
"status": "ready for upload",
"result": gin.H{
"id": uploadId,
},
})
}
}
func (ctrl UploadCtrl) CheckStatus(c *gin.Context) {
uploadId := c.Param("id")
exists, err := storage.IdExists(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if !exists {
utils.JSONErr(c, http.StatusNotFound, "upload id not found")
return
}
// Get upload status
uploadStatus, err := storage.GetUploadStatus(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if uploadStatus.IsNew() {
c.JSON(http.StatusOK, gin.H{
"status": uploadStatus,
})
return
}
// Get invoice id for upload
invoiceId, err := storage.GetUploadIdInvoiceId(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// No invoice found
if invoiceId == "" {
utils.JSONErr(c, http.StatusNotFound, "no invoice found")
return
}
// Get invoice
invoice, err := ln.CheckInvoice(invoiceId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
c.JSON(ln.InvoiceHttpCode(invoice), gin.H{
"upload_id": uploadId,
"invoice": invoice,
"status": uploadStatus,
})
}
func (ctrl UploadCtrl) PollStatus(c *gin.Context) {
uploadId := c.Param("id")
exists, err := storage.IdExists(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if !exists {
utils.JSONErr(c, http.StatusNotFound, "id not found")
return
}
uploadStatus, err := storage.GetUploadStatus(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if uploadStatus&storage.UpNew != 0 {
c.JSON(http.StatusCreated, gin.H{
"status": uploadStatus,
})
return
}
// we are not in UpNew state so we should have an invoice
invoice, err := storage.GetUploadInvoice(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if invoice == nil {
utils.JSONErr(c, http.StatusInternalServerError, "no invoice found")
return
}
if uploadStatus.WaitPay() {
invoiceChan := make(chan *ln.Invoice)
errorChan := make(chan error)
log.Printf("starting invoice poll for %s", invoice.RHash)
// If waiting payment, wait until invoice is paid
go ln.PollPaidInvoice(invoice.RHash, invoiceChan, errorChan)
// Block until payment done or error
log.Println("blocking")
select {
case invoice := <-invoiceChan:
log.Printf("poll: received invoice notif %s", invoice.RHash)
////////////////
///// Invoice was paid
////////////////
//
log.Println("POLL: invoice was paid")
uploadStatus |= storage.UpPaid
err := storage.SetUploadStatus(uploadId, uploadStatus)
if err != nil {
log.Printf("Redis error: %s", err)
}
// Update Upload Invoice
err = storage.SetUploadInvoice(uploadId, invoice)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Generate admin token and save it
adminToken, err := storage.GetLongId()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
err = storage.SetUploadAdminToken(uploadId, adminToken)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Get the download id
dlId, err := storage.GetDownloadId(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Clear the session
upSession, err := SessionStore.Get(c.Request, UpSessionKey)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
upSession.Flashes()
err = upSession.Save(c.Request, c.Writer)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// invoice was paid
c.JSON(http.StatusAccepted, gin.H{
"status": uploadStatus,
"invoice": invoice,
"admin_token": adminToken,
"download_id": dlId,
})
return
case err := <-errorChan:
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// We stopped waiting for the invoice, it must have expired
err := storage.SetUploadStatus(uploadId, storage.UpPayExpired)
if err != nil {
log.Printf("Redis error: %s", err)
}
c.JSON(http.StatusGone, gin.H{
"status": uploadStatus,
"invoice": invoice,
})
return
} else {
c.JSON(http.StatusOK, gin.H{
"status": uploadStatus,
"invoice": invoice,
})
}
return
}
func (ctrl UploadCtrl) Upload(c *gin.Context) {
// Check that upload ID exists
uploadId := c.Param("id")
exists, err := storage.IdExists(uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if !exists {
utils.JSONErr(c, http.StatusNotFound, "id not found")
return
}
// Check if payment done
// NOTE: we pre upload and remove the files if the payment was expired
//uploadStatus, err := GetUploadStatus(uploadId)
//if err != nil {
//utils.JSONErrPriv(c, http.StatusInternalServerError, err)
//return
//}
//if uploadStatus < UpWaitingStorage {
//utils.JSONErr(c, http.StatusPaymentRequired,
//fmt.Sprintf("invoice not paid"))
//return
//}
form, err := c.MultipartForm()
if err != nil {
utils.JSONErr(c, http.StatusNotAcceptable,
fmt.Sprintf("Could not parse form: %s", err.Error()))
return
}
files := form.File["upload[]"]
tx, err := db.DB.Sql.Beginx()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
for _, file := range files {
log.Printf("Handling %s", file.Filename)
// Get the file's sha256
hasher := sha256.New()
formFd, err := file.Open()
defer formFd.Close()
if err != nil {
utils.JSONErr(
c, http.StatusInternalServerError,
fmt.Sprintf("Could not read file %s", file.Filename),
)
db.RollbackTx(tx, nil)
return
}
_, err = io.Copy(hasher, formFd)
if err != nil {
utils.JSONErr(c, http.StatusInternalServerError,
"")
db.RollbackTx(tx, nil)
return
}
sum256 := hex.EncodeToString(hasher.Sum(nil))
// Get the file's metadata from upload table
up, err := storage.GetByHashID(sum256, uploadId)
if err != nil {
utils.JSONErrPriv(c, http.StatusNotFound, err)
db.RollbackTx(tx, nil)
return
}
log.Printf("Found received file's %s metadata in local database\n", up.FileName)
// If file already stored for this upload report conflict
if up.Stored {
utils.JSONErr(c, http.StatusConflict,
fmt.Sprintf("%s already uploaded", up.FileName))
return
}
//
//
log.Println("Storing file")
err = storage.StoreFormFile(formFd, up.SHA256+up.FileExt)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
db.RollbackTx(tx, nil)
return
}
log.Printf("%s stored at %s", up.FileName,
storage.GetStoreDestination(up.SHA256+up.FileExt))
// Setting status to stored
log.Println("Updating file upload stored status")
err = up.TxSetFileStored(tx)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
// Rollback and remove the file
db.RollbackTx(tx, func() {
err := os.Remove(storage.GetStoreDestination(up.SHA256 + up.FileExt))
if err != nil {
log.Println(err)
}
})
return
}
}
err = tx.Commit()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
// Set the whole transaction as stored
err = storage.SetUploadStatus(uploadId, storage.UpStored)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{
"status": http.StatusOK,
"store_status": storage.UpStored,
"upload_id": uploadId,
})
}

@ -1,4 +1,4 @@
package storage
package api
import "time"
@ -9,9 +9,17 @@ type File struct {
Type string `form:"type"` // MIME type
}
//TODO: field validation
type UploadForm struct {
Files []File `form:"files" binding:"required"`
// Ask payment
RequestPayment bool `json:"request_payment"`
RequestPaymentAmount float64 `json:"request_payment_amount" binding:"min=1"`
PaymentCurrency string `json:"payment_currency"`
// Default Date().toJSON() from js
TimeStamp time.Time `form:"timestamp"`
TimeStamp time.Time `json:"timestamp"`
}

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><sqlb_project><db path="/home/spike/projects/bit4sat/server/db-storage/bit4sat.sqlite" readonly="0" foreign_keys="1" case_sensitive_like="0" temp_store="0" wal_autocheckpoint="1000" synchronous="1"/><attached/><window><main_tabs open="structure browser query pragmas" current="1"/></window><tab_structure><column_width id="0" width="300"/><column_width id="1" width="0"/><column_width id="2" width="100"/><column_width id="3" width="2105"/><column_width id="4" width="0"/><expanded_item id="0" parent="1"/><expanded_item id="1" parent="1"/><expanded_item id="2" parent="1"/><expanded_item id="3" parent="1"/></tab_structure><tab_browse><current_table name="upload"/><default_encoding codec=""/><browse_table_settings><table schema="" name="" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk=""><sort/><column_widths/><filter_values/><conditional_formats/><display_formats/><hidden_columns/><plot_y_axes/></table><table schema="main" name="upload" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk=""><sort/><column_widths><column index="1" value="275"/><column index="2" value="176"/><column index="3" value="256"/></column_widths><filter_values/><conditional_formats/><display_formats><column index="7" value=""/></display_formats><hidden_columns/><plot_y_axes/></table><table schema="main" name="upload_status" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk=""><sort/><column_widths/><filter_values/><conditional_formats/><display_formats/><hidden_columns/><plot_y_axes/></table><table schema="main" name="upload_with_status" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk=""><sort/><column_widths><column index="7" value="0"/><column index="8" value="0"/></column_widths><filter_values/><conditional_formats/><display_formats/><hidden_columns><column index="7" value="1"/><column index="8" value="1"/></hidden_columns><plot_y_axes/></table></browse_table_settings></tab_browse><tab_sql><sql name="SQL 1">SELECT EXISTS (SELECT upload_id FROM upload WHERE upload_id = '1Ijbco3pYIrl2VzTslERxszEHG1');</sql><sql name="SQL 2">SELECT * FROM upload JOIN upload_status ON upload.status = upload_status.type;</sql><current_tab id="1"/></tab_sql></sqlb_project>
<?xml version="1.0" encoding="UTF-8"?><sqlb_project><db path="/home/spike/projects/bit4sat/db-storage/bit4sat.sqlite" readonly="0" foreign_keys="1" case_sensitive_like="0" temp_store="0" wal_autocheckpoint="1000" synchronous="1"/><attached/><window><main_tabs open="structure browser query pragmas" current="1"/></window><tab_structure><column_width id="0" width="300"/><column_width id="1" width="0"/><column_width id="2" width="100"/><column_width id="3" width="2105"/><column_width id="4" width="0"/><expanded_item id="0" parent="1"/><expanded_item id="1" parent="1"/><expanded_item id="2" parent="1"/><expanded_item id="3" parent="1"/></tab_structure><tab_browse><current_table name="upload"/><default_encoding codec=""/><browse_table_settings><table schema="" name="" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk=""><sort/><column_widths/><filter_values/><conditional_formats/><display_formats/><hidden_columns/><plot_y_axes/></table><table schema="main" name="upload" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk=""><sort/><column_widths><column index="1" value="275"/><column index="2" value="176"/><column index="3" value="256"/></column_widths><filter_values/><conditional_formats/><display_formats><column index="7" value=""/></display_formats><hidden_columns/><plot_y_axes/></table><table schema="main" name="upload_status" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk=""><sort/><column_widths/><filter_values/><conditional_formats/><display_formats/><hidden_columns/><plot_y_axes/></table><table schema="main" name="upload_with_status" show_row_id="0" encoding="" plot_x_axis="" unlock_view_pk=""><sort/><column_widths><column index="7" value="0"/><column index="8" value="0"/></column_widths><filter_values/><conditional_formats/><display_formats/><hidden_columns><column index="7" value="1"/><column index="8" value="1"/></hidden_columns><plot_y_axes/></table></browse_table_settings></tab_browse><tab_sql><sql name="SQL 1">SELECT EXISTS (SELECT upload_id FROM upload WHERE upload_id = '1Ijbco3pYIrl2VzTslERxszEHG1');</sql><sql name="SQL 2">SELECT * FROM upload JOIN upload_status ON upload.status = upload_status.type;</sql><current_tab id="1"/></tab_sql></sqlb_project>

@ -0,0 +1,80 @@
package btc
import (
"encoding/json"
"errors"
"fmt"
"log"
"math"
"net/http"
"net/url"
"git.sp4ke.com/sp4ke/bit4sat/config"
)
const (
BitcoinAverageAPI = "https://apiv2.bitcoinaverage.com"
BTCMsatRatio = float64(100000000000)
)
var FixedRates = map[string]float64{
"BTC": 1,
}
func FiatToMsat(currency string, amount float64) (int64, error) {
rate, err := getRate(currency)
if err != nil {
return -1, err
}
return int64(math.Round((amount / rate) * BTCMsatRatio)), nil
}
func getRate(currency string) (float64, error) {
pairName := "BTC" + currency
reqParams := url.Values{}
reqParams.Set("crypto", "BTC")
reqParams.Set("fiat", currency)
reqUri := fmt.Sprintf("%s/indices/global/ticker/short?%s",
BitcoinAverageAPI, reqParams.Encode())
// TODO: remove on prod and use normal client
// Used for connectivity problems
client := &http.Client{}
if config.Env == "dev" {
proxyUrl, err := url.Parse(config.HttpProxy)
if err != nil {
log.Fatal(err)
}
client.Transport = &http.Transport{
Proxy: http.ProxyURL(proxyUrl),
}
}
resp, err := client.Get(reqUri)
if err != nil {
return -1, err
}
var data map[string]interface{}
dec := json.NewDecoder(resp.Body)
err = dec.Decode(&data)
if err != nil {
log.Fatal(err)
}
pair := data[pairName]
price, ok := pair.(map[string]interface{})["last"].(float64)
if !ok {
return -1, errors.New("bitcoinaverage parsing result")
}
return price, nil
}

@ -0,0 +1,2 @@
//TODO: cache price rates for faster invoices
package btc

@ -0,0 +1,22 @@
package bus
// Channel names
const (
WatchInvoicesChannelName = "watch_invoices"
WebsocketPubSubPrefix = "websocket_update"
UploadUpdateChannelPrefix = "upload_update"
InvoicePaidChannelPrefix = "invoice@paid"
)
// PubSub event types
const (
PaymentReceived = iota
SetUploadId
)
// Simple message used for update notifications in pub/sub
type Message struct {
UploadId string `json:"upload_id"`
Type int `json:"type"` // update type
Data interface{} `json:"data"`
}

@ -0,0 +1,75 @@
package config
import (
"fmt"
"github.com/namsral/flag"
)
const (
ChargeCallbackEndpoint = "chargecallback"
ShortIdMinLength = 8
LongIdMinLength = 32
)
var (
Env,
StoragePath,
ApiPort,
ApiInterface,
ApiListen,
ApiHost,
SessionSecret,
LnChargeApi,
LnChargeToken,
LndGrpcHost,
LndGrpcPort,
LndCertPath,
LndAdminMacaroonPath,
LockMacaroonIp,
ShortIdSalt,
HttpProxy,
SqlDbHost,
SqlDbUser,
SqlDbPass string
)
func init() {
// Storage
flag.StringVar(&StoragePath, "bit4sat-storage-path", "file-storage", "base file storage path")
// Api
flag.StringVar(&ApiPort, "api-port", "8880", "api port number")
flag.StringVar(&ApiInterface, "api-interface", "", "api listening interface")
flag.StringVar(&ApiHost, "api-host", "", "reachable hostname for api (used by callbacks)")
ApiListen = fmt.Sprintf("%s:%s", ApiInterface, ApiPort)
flag.StringVar(&SessionSecret, "session-secret", "default-secret", "cookie sessions secret")
// Database
flag.StringVar(&SqlDbHost, "sql-db-host", "bit4sat", "sql db hostname")
flag.StringVar(&SqlDbUser, "sql-db-user", "bit4sat", "sql db username")
flag.StringVar(&SqlDbPass, "sql-db-pass", "bit4sat", "sql db pass")
// LN Charge
flag.StringVar(&LnChargeApi, "ln-charge-api", "", "ln charge api endpoint")
flag.StringVar(&LnChargeToken, "ln-charge-token", "", "ln charge api token")
// LND
flag.StringVar(&LndGrpcHost, "lnd-grpc-host", "", "lnd host address")
flag.StringVar(&LndGrpcPort, "lnd-grpc-port", "", "lnd port address")
flag.StringVar(&LndCertPath, "lnd-cert-path", "", "lnd tls cert path")
flag.StringVar(&LndAdminMacaroonPath, "lnd-macaroon-admin-path", "", "admin macaroon path")
flag.StringVar(&LockMacaroonIp, "lock-macaroon-ip", "", "lock macaroon to ip address")
flag.StringVar(&HttpProxy, "http-proxy", "", "http proxy for clients")
flag.StringVar(&ShortIdSalt, "short-id-salt", "bit4sat-23591", "hashid salt")
flag.StringVar(&Env, "env", "dev", "dev or prod")
//log.Printf("locking macaroon to ip %s", LockMacaroonIp)
flag.Parse()
}

@ -3,67 +3,52 @@ package db
import (
"fmt"
"log"
"net/url"
"os"
"path/filepath"
"git.sp4ke.com/sp4ke/bit4sat-server.git/utils"
"git.sp4ke.com/sp4ke/bit4sat/config"
_ "github.com/lib/pq"
//_ "github.com/go-sql-driver/mysql"
"github.com/jmoiron/sqlx"
_ "github.com/mattn/go-sqlite3"
"github.com/mediocregopher/radix/v3"
)
const (
DBName = "bit4sat.sqlite"
DBPath = "db-storage"
DBPathEnv = "BIT4SAT_DB_PATH"
DBPragma = ` PRAGMA foreign_keys = ON; `
DBName = "bit4sat"
)
var (
DBOptions = map[string]string{
"_journal_mode": "WAL",
}
DB *Database
)
type Database struct {
Handle *sqlx.DB
Sql *sqlx.DB
Redis *radix.Pool
RedisPubSub radix.PubSubConn
}
func (d *Database) Open() error {
var err error
dsnOptions := &url.Values{}
for k, v := range DBOptions {
dsnOptions.Set(k, v)
}
dsn := fmt.Sprintf("postgres://%s:%s@%s/%s?sslmode=disable",
config.SqlDbUser,
config.SqlDbPass,
config.SqlDbHost,
DBName)
// Get db base path
path, set := os.LookupEnv(DBPathEnv)
if !set {
path = DBPath
}
log.Printf("Opening SQL at %s\n", dsn)
// Create path if not exists
err := utils.Mkdir(path)
if err != nil {
log.Fatal(err)
}
path = filepath.Join(path, DBName)
//path = fmt.Sprintf("%s/%s", path, DBName)
dsn := fmt.Sprintf("file:%s?%s", path, dsnOptions.Encode())
d.Sql = sqlx.MustConnect("postgres", dsn)
log.Printf("Opening sqlite db %s\n", dsn)
// Execute Pragmas
//d.Sql.MustExec(DBPragma)
d.Handle, err = sqlx.Open("sqlite3", dsn)
// Opend redis
d.Redis, err = radix.NewPool("tcp", "redis:6379", 10)
if err != nil {
log.Fatal(err)
}
// Execute Pragmas
d.Handle.MustExec(DBPragma)
d.RedisPubSub = radix.PersistentPubSub("tcp", "redis:6379", nil)
return nil
}
@ -83,4 +68,5 @@ func RollbackTx(tx *sqlx.Tx, callback func()) {
func init() {
DB = &Database{}
DB.Open()
}

@ -0,0 +1,24 @@
package db
import (
"github.com/mediocregopher/radix/v3"
)
func GetFromKey(key string, target interface{}) error {
return DB.Redis.Do(radix.FlatCmd(target, "GET", key))
}
func SetKeyVal(key string, val interface{}) error {
return DB.Redis.Do(radix.FlatCmd(nil, "SET", key, val))
}
func ExpireKey(key string, seconds int) error {
return DB.Redis.Do(radix.FlatCmd(nil, "EXPIRE", key, seconds))
}
func Exists(key string) (bool, error) {
var exists bool
err := DB.Redis.Do(radix.Cmd(&exists, "EXISTS", key))
return exists, err
}

@ -0,0 +1 @@
Subproject commit 59829936cca04bc9c9b603d0140c2b6ff6f2de52

@ -1,28 +0,0 @@
version: "3.4"
volumes:
redis:
name: bit4sat-data
services:
bit4sat:
image: sp4ke/bit4sat
build:
context: .
environment:
- GO111MODULE=on
deploy:
replicas: 1
volumes:
- $PWD:/src
working_dir: /src
command:
- go
- "run *.go"

@ -3,19 +3,31 @@ module git.sp4ke.com/sp4ke/bit4sat
go 1.12
require (
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff
github.com/garyburd/redigo v1.6.0 // indirect
github.com/gin-contrib/cors v0.0.0-20190301062745-f9e10995c85a
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 // indirect
github.com/gin-contrib/sessions v0.0.0-20190226023029-1532893d996f
github.com/gin-gonic/gin v1.3.0
github.com/go-sql-driver/mysql v1.4.1 // indirect
github.com/golang/protobuf v1.3.1 // indirect
github.com/gorilla/websocket v1.4.0
github.com/jmoiron/sqlx v1.2.0
github.com/kr/pretty v0.1.0 // indirect
github.com/lib/pq v1.0.0
github.com/lightningnetwork/lnd v0.5.2-beta
github.com/mattn/go-isatty v0.0.7 // indirect
github.com/mattn/go-sqlite3 v1.10.0
github.com/mattn/go-sqlite3 v1.10.0 // indirect
github.com/mediocregopher/radix/v3 v3.2.3
github.com/namsral/flag v1.7.4-pre
github.com/segmentio/ksuid v1.0.2
github.com/speps/go-hashids v2.0.0+incompatible
github.com/stretchr/testify v1.3.0 // indirect
github.com/ugorji/go/codec v0.0.0-20190315113641-a70535d8491c // indirect
golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53 // indirect
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54 // indirect
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 // indirect
golang.org/x/net v0.0.0-20190322120337-addf6b3196f6
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc // indirect
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 // indirect
google.golang.org/appengine v1.5.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 // indirect
google.golang.org/grpc v1.19.1
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b // indirect
gopkg.in/macaroon.v2 v2.1.0
)

217
go.sum

@ -1,22 +1,130 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e h1:F2x1bq7RaNCIuqYpswggh1+c1JmwdnkHNC9wy1KDip0=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NebulousLabs/fastrand v0.0.0-20180208210444-3cf7173006a0 h1:g/ETZwHx5wN2fqKWS3gCUrEU7dLko+DvVs3hakQCfyE=
github.com/NebulousLabs/fastrand v0.0.0-20180208210444-3cf7173006a0/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 h1:MG93+PZYs9PyEsj/n5/haQu2gK0h4tUtSy9ejtMwWa0=
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc=
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2 h1:2be4ykKKov3M1yISM2E8gnGXZ/N2SsPawfnGiXxaYEU=
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/btcsuite/btcd v0.0.0-20180823030728-d81d8877b8f3/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
github.com/btcsuite/btcd v0.0.0-20180824064422-ed77733ec07dfc8a513741138419b8d9d3de9d2d h1:JyAoNOSAG3StMRTiI8M5ReA70DEWoTGb/tuaxYdFWGU=
github.com/btcsuite/btcd v0.0.0-20180824064422-ed77733ec07dfc8a513741138419b8d9d3de9d2d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589 h1:9A5pe5iQS+ll6R1EVLFv/y92IjrymihwITCU81aCIBQ=
github.com/btcsuite/btcutil v0.0.0-20190112041146-bf1e1be93589/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcwallet v0.0.0-20180904010540-284e2e0e696e33d5be388f7f3d9a26db703e0c06/go.mod h1:/d7QHZsfUAruXuBhyPITqoYOmJ+nq35qPsJjz/aSpCg=
github.com/btcsuite/btcwallet v0.0.0-20190123033236-ba03278a64bc h1:E7lDde/zAxAfvF750wMP0pUIAzF+wtwO2jQRy++q60U=
github.com/btcsuite/btcwallet v0.0.0-20190123033236-ba03278a64bc/go.mod h1:+u1ftn+QOb9qHKwsLf7rBOr0PHCo9CGA7U1WFq7VLA4=
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941 h1:kij1x2aL7VE6gtx8KMIt8PGPgI5GV9LgtHFG5KaEMPY=
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:QcFA8DZHtuIAdYKCq/BzELOaznRsCvwf4zTPmaYwaig=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8 h1:nOsAWScwueMVk/VLm/dvQQD7DuanyvAUb6B3P3eT274=
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820 h1:W1bWzjKRrqKEpWlFsJ6Yef9Q4LUhdfJmS6sQrQj5L6c=
github.com/coreos/bbolt v0.0.0-20180223184059-7ee3ded59d4835e10f3e7d0f7603c42aa5e83820/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.0.0 h1:QgmxFbprE29UG4oL88tGiiL/7VuiBl5xCcz+wJcJhc0=
github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/garyburd/redigo v1.6.0 h1:0VruCpn7yAIIu7pWVClQC8wxCJEcG3nyzpMSHKi1PQc=
github.com/garyburd/redigo v1.6.0/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
github.com/gin-contrib/cors v0.0.0-20190301062745-f9e10995c85a h1:zBycVvXa03SIX+jdMv8wGu9TMDMWdN8EhaR1FoeKHNo=
github.com/gin-contrib/cors v0.0.0-20190301062745-f9e10995c85a/go.mod h1:pL2kNE+DgDU+eQ+dary5bX0Z6LPP8nR6Mqs1iejILw4=
github.com/gin-contrib/sessions v0.0.0-20190226023029-1532893d996f h1:f8TGHcU6cxOhMwW6YQhpRe+zlr05qNjVmdcK1qigr5I=
github.com/gin-contrib/sessions v0.0.0-20190226023029-1532893d996f/go.mod h1:8Xd9k6zfW7ekJjy3wJrgbgB2KWvP+GWywe6PanyMVwI=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v0.0.0-20180821051752-b27b920f9e71/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc h1:3NXdOHZ1YlN6SGP3FPbn4k73O2MeEp065abehRwGFxI=
github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jackpal/gateway v1.0.4 h1:LS5EHkLuQ6jzaHwULi0vL+JO0mU/n4yUtK8oUjHHOlM=
github.com/jackpal/gateway v1.0.4/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad h1:heFfj7z0pGsNCekUlsFhO2jstxO4b5iQ665LjwM5mDc=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19 h1:k9/LaykApavRKKlaWkunBd48Um+vMxnUNNsIjS7OJn8=
github.com/jessevdk/go-flags v0.0.0-20170926144705-f88afde2fa19/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmoiron/sqlx v1.2.0 h1:41Ip0zITnmWNR/vHV+S4m+VoUivnWY5E4OJfLZjCJMA=
github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhBSsks=
github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE=
github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/juju/clock v0.0.0-20180808021310-bab88fc67299 h1:K9nBHQ3UNqg/HhZkQnGG2AE4YxDyNmGS9FFT2gGegLQ=
github.com/juju/clock v0.0.0-20180808021310-bab88fc67299/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 h1:rhqTjzJlm7EbkELJDKMTU7udov+Se0xZkWmugr6zGok=
github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20180524022052-584905176618 h1:MK144iBQF9hTSwBW/9eJm034bVoG30IshVm688T2hi8=
github.com/juju/loggo v0.0.0-20180524022052-584905176618/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216 h1:/eQL7EJQKFHByJe3DeE8Z36yqManj9UY5zppDoQi4FU=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073 h1:WQM1NildKThwdP7qWrNAFGzp4ijNLw8RlgENkaI4MJs=
github.com/juju/testing v0.0.0-20180920084828-472a3e8b2073/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d h1:irPlN9z5VCe6BTsqVsxheCZH99OFSmqSVyTigW4mEoY=
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305 h1:lQxPJ1URr2fjsKnJRt/BxiIxjLt9IKGvS+0injMHbag=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -24,50 +132,139 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885 h1:fTLuPUkaKIIV0+gA1IxiBDvDxtF8tzpSF6N6NfFGmsU=
github.com/lightninglabs/gozmq v0.0.0-20180324010646-462a8a753885/go.mod h1:KUh15naRlx/TmUMFS/p4JJrCrE6F7RGF7rsnvuu45E4=
github.com/lightninglabs/neutrino v0.0.0-20181017011010-4d6069299130/go.mod h1:KJq43Fu9ceitbJsSXMILcT4mGDNI/crKmPIkDOZXFyM=
github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af h1:JzoYbWqwPb+PARU4LTtlohetdNa6/ocyQ0xidZQw4Hg=
github.com/lightninglabs/neutrino v0.0.0-20190115022559-351f5f06c6af/go.mod h1:aR+E6cs+FTaIwIa/WLyvNsB8FZg8TiP3r0Led+4Q4gI=
github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6 h1:ONLGrYJVQdbtP6CE/ff1KNWZtygRGEh12RzonTiCzPs=
github.com/lightningnetwork/lightning-onion v0.0.0-20180605012408-ac4d9da8f1d6/go.mod h1:8EgEt4a/NUOVQd+3kk6n9aZCJ1Ssj96Pb6lCrci+6oc=
github.com/lightningnetwork/lnd v0.5.2-beta h1:AecJ2HFtQgktVPgpxViP0/LEFAN73MsWGjtSviZgMU0=
github.com/lightningnetwork/lnd v0.5.2-beta/go.mod h1:tkYuDSrt0DPsObEYGBOgycuRzy1bWm89KOFxVaAOBHI=
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw=
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY=
github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK860o=
github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed h1:3dQJqqDouawQgl3gBE1PNHKFkJYGEuFb1DbSlaxdosE=
github.com/mediocregopher/mediocre-go-lib v0.0.0-20181029021733-cb65787f37ed/go.mod h1:dSsfyI2zABAdhcbvkXqgxOxrCsbYeHCPgrZkku60dSg=
github.com/mediocregopher/radix/v3 v3.2.3 h1:TbcGCZdo9zfPYPgevsqRn+OjvCyfOK6TzuXhqzWdCt0=
github.com/mediocregopher/radix/v3 v3.2.3/go.mod h1:EmfVyvspXz1uZEyPBMyGK+kjWiKQGvsUt6O3Pj+LDCQ=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/namsral/flag v1.7.4-pre h1:b2ScHhoCUkbsq0d2C15Mv+VU8bl8hAXV8arnWiOHNZs=
github.com/namsral/flag v1.7.4-pre/go.mod h1:OXldTctbM6SWH1K899kPZcf65KxJiD7MsceFUpB5yDo=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quasoft/memstore v0.0.0-20180925164028-84a050167438/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af h1:gu+uRPtBe88sKxUCEXRoeCvVG90TJmwhiqRpvdhQFng=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/segmentio/ksuid v1.0.2 h1:9yBfKyw4ECGTdALaF09Snw3sLJmYIX6AbPJrAy6MrDc=
github.com/segmentio/ksuid v1.0.2/go.mod h1:BXuJDr2byAiHuQaQtSKoXh1J0YmUDurywOXgB2w+OSU=
github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw=
github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs=
github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2 h1:EICbibRW4JNKMcY+LsWmuwob+CRS1BmdRdjphAm9mH4=
github.com/ugorji/go/codec v0.0.0-20181209151446-772ced7fd4c2/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/ugorji/go/codec v0.0.0-20190315113641-a70535d8491c h1:20Yyqg0mvFOyG3m7ejVqHEa03CBa3hTttx9jiYLkjYU=
github.com/ugorji/go/codec v0.0.0-20190315113641-a70535d8491c/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 h1:vG/gY/PxA3v3l04qxe3tDjXyu3bozii8ulSlIPOYKhI=
github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA=
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
go.etcd.io/bbolt v1.3.0 h1:oY10fI923Q5pVCVt1GBTZMn8LHo5M+RCInFpeMnV4QI=
go.etcd.io/bbolt v1.3.0/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181127143415-eb0de9b17e85/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180821023952-922f4815f713/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53 h1:kcXqo9vE6fsZY5X5Rd7R1l7fTgnWaDCVmln65REefiE=
golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190322120337-addf6b3196f6 h1:78jEq2G3J16aXneH23HSnTQQTCwMHoyO8VEiUH+bpPM=
golang.org/x/net v0.0.0-20190322120337-addf6b3196f6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180821140842-3b58ed4ad339/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54 h1:xe1/2UUJRmA9iDglQSlkx8c5n3twv58+K0mPpC2zmhA=
golang.org/x/sys v0.0.0-20190321052220-f7bb7a8bee54/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc h1:4gbWbmmPFp4ySWICouJl6emP0MyS31yy9SrTlAGFT+g=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.5.0 h1:KxkO13IPW4Lslp2bz+KHP2E3gtFlrIGNThxkZQ3g+4c=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181127195345-31ac5d88444a/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922 h1:mBVYJnbrXLA/ZCBTCe7PtEgAUP+1bg92qTaFoPHdz+8=
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
google.golang.org/grpc v1.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.19.1 h1:TrBcJ1yqAl1G++wO39nD/qtgpsW9/1+QGrluyMGEYgM=
google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b h1:U/Uqd1232+wrnHOvWNaxrNqn/kFnr4yu4blgPtQt0N8=
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b/go.mod h1:fgfIZMlsafAHpspcks2Bul+MWUNw/2dyQmjC2faKjtg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v1 v1.0.0 h1:n+7XfCyygBFb8sEjg6692xjC6Us50TFRO54+xYUEwjE=
gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
gopkg.in/macaroon-bakery.v2 v2.0.1 h1:0N1TlEdfLP4HXNCg7MQUMp5XwvOoxk+oe9Owr2cpvsc=
gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA=
gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I=
gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI=
gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce h1:xcEWjVhvbDy+nHP67nPDDpbYrY+ILlfndk4bRioVHaU=
gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

@ -0,0 +1,127 @@
package ln
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"git.sp4ke.com/sp4ke/bit4sat/btc"
"git.sp4ke.com/sp4ke/bit4sat/bus"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/lndrpc"
"git.sp4ke.com/sp4ke/bit4sat/utils"
"github.com/mediocregopher/radix/v3"
)
var (
Client *http.Client
)
// Quick check if invoice was paid
func CheckInvoice(id string) (*Invoice, error) {
lnInvoice, err := lndrpc.LookupInvoiceRhashStr(id)
if err != nil {
return nil, err
}
invoice := InvoiceFromLndIn(lnInvoice)
return invoice, nil
}
// This will watch on the pubsub for paid invoices given an invoice id
func PollPaidInvoice(invoiceId string, invoicePaidChan chan<- *Invoice, errorChan chan<- error) {
defer func() {
log.Printf("quitting poller %s", invoiceId)
}()
watchPaidChannel := make(chan radix.PubSubMessage)
busName := fmt.Sprintf("%s:%s", bus.InvoicePaidChannelPrefix, invoiceId)
err := db.DB.RedisPubSub.Subscribe(watchPaidChannel, busName)
if err != nil {
log.Printf("error in poll invoice: %s", err)
errorChan <- err
return
}
log.Printf("poller: %s, listening to watchPaidChannel", invoiceId)
for msg := range watchPaidChannel {
log.Printf("poller received message on %s", msg.Channel)
split := strings.Split(msg.Channel, ":")
if len(split) != 2 {
errorChan <- fmt.Errorf("error in pubsub channel parsing %s", msg.Channel)
}
targetInvoiceId := strings.Split(msg.Channel, ":")[1]
log.Printf("target invoice is %s", targetInvoiceId)
if targetInvoiceId == invoiceId {
invoice := Invoice{}
err := json.Unmarshal(msg.Message, &invoice)
if err != nil {
errorChan <- err
return
}
invoicePaidChan <- &invoice
close(invoicePaidChan)
return
}
}
}
type InvoiceOpts struct {
Amount float64
Curr Currency
Memo string
}
func NewInvoice(opts InvoiceOpts) (*Invoice, error) {
var err error
var satValue int64
if opts.Curr == CurMSat {
log.Println("cur is msat")
// Convert to MSAT, meaning values under 1000 MSAT are ignored
satValue = int64(opts.Amount / 1000)
// Other supported currecies
}
if opts.Curr != CurSat {
log.Println("cur is custom")
// Get Sat satValue for this invoice
msatVal, err := btc.FiatToMsat(CurrencyString[opts.Curr], opts.Amount)
if err != nil {
return nil, err
}
satValue = msatVal / 1000
} else {
satValue = int64(opts.Amount)
}
lndInvoice, err := lndrpc.AddInvoiceSat(opts.Memo, satValue)
if err != nil {
return nil, err
}
invoice := InvoiceFromLndIn(lndInvoice)
invoice.QuotedCurrency = CurrencyString[opts.Curr]
invoice.QuotedAmount = opts.Amount
return invoice, nil
}
func init() {
Client = utils.NewHttpClient()
}

@ -0,0 +1,191 @@
package ln
import (
"encoding/hex"
"encoding/json"
"log"
"strconv"
"strings"
"time"
"github.com/lightningnetwork/lnd/lnrpc"
)
type Currency int
const (
CurMSat Currency = iota
CurSat
CurBTC
CurUSD
CurEur
)
const (
Paid int = iota
UnPaid
Expired
)
var InvoiceStatus = map[int]string{
UnPaid: "unpaid",
Paid: "paid",
Expired: "expired",
}
const (
InfoEndpoint = "info"
InvoiceEndpoint = "invoice"
)
var (
CurrencyString = map[Currency]string{
CurUSD: "USD",
CurSat: "SAT",
CurBTC: "BTC",
CurMSat: "MSAT",
CurEur: "EUR",
}
CurrencyID = map[string]Currency{
"USD": CurUSD,
"SAT": CurSat,
"MSAT": CurMSat,
"BTC": CurBTC,
"EUR": CurEur,
}
)
type timestamp time.Time
func (t *timestamp) UnmarshalJSON(in []byte) error {
if string(in) == "null" {
*t = timestamp(time.Unix(0, 0))
return nil
}
// Try to first parse in RFC3339
var val time.Time
var err error
// put the input in string mode
strIn := string(in)
// clean
strIn = strings.Replace(strIn, "\"", "", -1)
if val, err = time.Parse(time.RFC3339, strIn); err != nil {
val, err := strconv.Atoi(strIn)
if err != nil {
log.Printf("error unmarshal time %s", in)
return err
}
parsedTime := time.Unix(int64(val), 0)
*t = timestamp(parsedTime)
return nil
}
*t = timestamp(val)
return nil
}
func (t timestamp) MarshalJSON() ([]byte, error) {
str := strconv.Itoa(int(time.Time(t).Unix()))
return []byte(str), nil
}
func (t timestamp) String() string {
return time.Time(t).Format(time.RFC3339)
}
type Invoice struct {
AddIndex uint64 `json:"-"`
SettleIndex uint64 `json:"-"`
Description string `json:"description"`
Msatoshi float64 `json:"msatoshi"`
Payreq string `json:"payreq"`
RHash string `json:"rhash"`
PreImage string `json:"-"` // used as secret admin token
Status string `json:"status"`
Settled bool `json:"settled"`
PaidAt timestamp `json:"paid_at"`
CreatedAt timestamp `json:"created_at"`
ExpiresAt timestamp `json:"expires_at"`
Expired bool `json:"-"`
QuotedCurrency string `json:"-"`
QuotedAmount float64 `json:"-"`
}
func (i Invoice) MarshalBinary() ([]byte, error) {
return json.Marshal(i)
}
func (i *Invoice) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, &i)
}
// Create new Invoice from lnrpc.Invoice
func InvoiceFromLndIn(lndInvoice *lnrpc.Invoice) *Invoice {
//log.Printf("new invoice form lnd with value %d", lndInvoice.Value)
//log.Printf("setting msat to %f", float64(lndInvoice.Value*1000))
invoice := Invoice{
AddIndex: lndInvoice.AddIndex,
Description: lndInvoice.GetMemo(),
Msatoshi: float64(lndInvoice.Value * 1000),
Payreq: lndInvoice.PaymentRequest,
RHash: hex.EncodeToString(lndInvoice.RHash),
PreImage: hex.EncodeToString(lndInvoice.RPreimage),
CreatedAt: timestamp(time.Unix(lndInvoice.CreationDate, 0)),
PaidAt: timestamp(time.Unix(lndInvoice.SettleDate, 0)),
Status: InvoiceStatus[UnPaid],
}
invoice.ExpiresAt = timestamp(time.Time(invoice.CreatedAt).Add(time.Second * time.Duration(lndInvoice.Expiry)))
// Calculate status
if lndInvoice.Settled {
invoice.Settled = true
invoice.Status = InvoiceStatus[Paid]
}
if time.Now().After(time.Time(invoice.ExpiresAt)) {
invoice.Status = InvoiceStatus[Expired]
invoice.Expired = true
}
return &invoice
}
// Update stored invoice from lnd invoice
func UpdateInvoiceFromLnd(storedIn *Invoice, newIn *lnrpc.Invoice) *Invoice {
storedIn.SettleIndex = newIn.SettleIndex
storedIn.PaidAt = timestamp(time.Unix(newIn.SettleDate, 0))
// Calculate status
if newIn.Settled {
storedIn.Status = InvoiceStatus[Paid]
storedIn.Settled = true
}
// Handle expired status
if time.Now().After(time.Time(storedIn.ExpiresAt)) {
storedIn.Status = InvoiceStatus[Expired]
storedIn.Expired = true
}
return storedIn
}
func IsExpiredInvoice(creation, expiry int64) bool {
createdAt := time.Unix(int64(creation), 0)
expiresAt := createdAt.Add(time.Second * time.Duration(expiry))
return time.Now().After(expiresAt)
}

@ -0,0 +1,18 @@
package ln
import (
"net/http"
)
func InvoiceHttpCode(invoice *Invoice) int {
if invoice.Expired {
return http.StatusGone
}
if invoice.Status == "unpaid" {
return http.StatusPaymentRequired
}
return http.StatusOK
}

@ -0,0 +1,84 @@
package lndrpc
import (
"log"
"golang.org/x/net/context"
lnrpc "github.com/lightningnetwork/lnd/lnrpc"
)
func GetInfo() error {
log.Println("get info")
ctxb := context.Background()
client, cleanUp := GetClient()
defer cleanUp()
req := &lnrpc.GetInfoRequest{}
resp, err := client.GetInfo(ctxb, req)
if err != nil {
return err
}
log.Println(resp)
return nil
}
func lookupInvoiceRhash(rhash []byte) (*lnrpc.Invoice, error) {
ctxb := context.Background()
client, cleanUp := GetClient()
defer cleanUp()
req := &lnrpc.PaymentHash{
RHash: rhash,
}
// Get back the invoice
createdInvoice, err := client.LookupInvoice(ctxb, req)
if err != nil {
return nil, err
}
return createdInvoice, nil
}
func LookupInvoiceRhashStr(rhash string) (*lnrpc.Invoice, error) {
ctxb := context.Background()
client, cleanUp := GetClient()
defer cleanUp()
req := &lnrpc.PaymentHash{
RHashStr: rhash,
}
// Get back the invoice
createdInvoice, err := client.LookupInvoice(ctxb, req)
if err != nil {
return nil, err
}
return createdInvoice, nil
}
func AddInvoiceSat(desc string, satVal int64) (*lnrpc.Invoice, error) {
ctxb := context.Background()
client, cleanUp := GetClient()
defer cleanUp()
invoice := &lnrpc.Invoice{
Memo: desc,
// Value in satoshis
Value: satVal,
}
resp, err := client.AddInvoice(ctxb, invoice)
if err != nil {
return nil, err
}
return lookupInvoiceRhash(resp.RHash)
}

@ -0,0 +1,108 @@
// Grpc interface to lnd
package lndrpc
import (
"fmt"
"io/ioutil"
"log"
"github.com/lightningnetwork/lnd/lncfg"
"github.com/lightningnetwork/lnd/macaroons"
macaroon "gopkg.in/macaroon.v2"
"git.sp4ke.com/sp4ke/bit4sat/config"
"github.com/lightningnetwork/lnd/lnrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
func fatal(err error) {
log.Fatal(err)
}
func GetClient() (lnrpc.LightningClient, func()) {
conn := getClientConn()
cleanUp := func() {
conn.Close()
}
return lnrpc.NewLightningClient(conn), cleanUp
}
func getClientConn() *grpc.ClientConn {
// Load the specified TLS certificate and build transport credentials
// with it.
creds, err := credentials.NewClientTLSFromFile(config.LndCertPath, "")
if err != nil {
fatal(err)
}
// Create a dial options array.
opts := []grpc.DialOption{
grpc.WithTransportCredentials(creds),
}
// Load the specified macaroon file.
macBytes, err := ioutil.ReadFile(config.LndAdminMacaroonPath)
if err != nil {
fatal(fmt.Errorf("unable to read macaroon path (check "+
"the network setting!): %v", err))
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macBytes); err != nil {
fatal(fmt.Errorf("unable to decode macaroon: %v", err))
}
macConstraints := []macaroons.Constraint{
// We add a time-based constraint to prevent replay of the
// macaroon. It's good for 60 seconds by default to make up for
// any discrepancy between client and server clocks, but leaking
// the macaroon before it becomes invalid makes it possible for
// an attacker to reuse the macaroon. In addition, the validity
// time of the macaroon is extended by the time the server clock
// is behind the client clock, or shortened by the time the
// server clock is ahead of the client clock (or invalid
// altogether if, in the latter case, this time is more than 60
// seconds).
// TODO(aakselrod): add better anti-replay protection.
macaroons.TimeoutConstraint(60),
// Lock macaroon down to a specific IP address.
//macaroons.IPLockConstraint(config.LockMacaroonIp),
macaroons.IPLockConstraint(""),
// ... Add more constraints if needed.
}
// Apply constraints to the macaroon.
constrainedMac, err := macaroons.AddConstraints(mac, macConstraints...)
if err != nil {
fatal(err)
}
// Now we append the macaroon credentials to the dial options.
cred := macaroons.NewMacaroonCredential(constrainedMac)
opts = append(opts, grpc.WithPerRPCCredentials(cred))
// We need to use a custom dialer so we can also connect to unix sockets
// and not just TCP addresses.
opts = append(
opts, grpc.WithDialer(
lncfg.ClientAddressDialer(config.LndGrpcPort),
),
)
conn, err := grpc.Dial(fmt.Sprintf("%s:%s", config.LndGrpcHost, config.LndGrpcPort), opts...)
if err != nil {
fatal(fmt.Errorf("unable to connect to RPC server: %v", err))
}
return conn
}
func init() {
log.Println("init grpc")
}

@ -1,11 +1,19 @@
package main
import "git.sp4ke.com/sp4ke/bit4sat-server.git/db"
import (
"git.sp4ke.com/sp4ke/bit4sat/api"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/watchers"
)
func main() {
defer db.DB.Handle.Close()
api := NewAPI()
api.Run()
go watchers.WatchInvoice()
defer db.DB.Sql.Close()
v1 := api.NewAPI()
defer api.SessionStore.Close()
v1.Run()
}

@ -0,0 +1,5 @@
{
"dependencies": {
"socket.io": "^2.2.0"
}
}

@ -0,0 +1,54 @@
package storage
import (
"time"
"git.sp4ke.com/sp4ke/bit4sat/config"
"github.com/speps/go-hashids"
)
var ShortHD *hashids.HashIDData
var LongHD *hashids.HashIDData
func GetShortId() (string, error) {
hd, err := hashids.NewWithData(ShortHD)
if err != nil {
return "", err
}
now := int64(time.Now().UnixNano())
short, err := hd.EncodeInt64([]int64{now})
if err != nil {
return "", err
}
return short, nil
}
func GetLongId() (string, error) {
hd, err := hashids.NewWithData(LongHD)
if err != nil {
return "", err
}
now := int64(time.Now().UnixNano())
long, err := hd.EncodeInt64([]int64{now})
if err != nil {
return "", err
}
return long, nil
}
func init() {
ShortHD = hashids.NewData()
ShortHD.Salt = config.ShortIdSalt
ShortHD.MinLength = 8
LongHD = hashids.NewData()
LongHD.Salt = config.ShortIdSalt
LongHD.MinLength = 32
}

@ -0,0 +1,28 @@
package storage
import (
"testing"
)
func TestUploadStatus(t *testing.T) {
// start with new status
//
//status := UpPaid | UpStored
//j, _ := json.Marshal(status)
//fmt.Printf("%s", j)
//fmt.Printf("%32b\n", UpPaid|UpStored)
//err := SetUploadStatus("1", status)
//if err != nil {
//t.Error(err)
//}
// Set to stored
//status |= UpStored
//fmt.Println(status)
//if !status.IsStored() {
//t.Error()
//}
}

@ -9,20 +9,12 @@ import (
"os"
"path/filepath"
"git.sp4ke.com/sp4ke/bit4sat-server.git/utils"
)
const (
StoragePath = "file-storage"
StoragePathEnv = "BIT4SAT_STORAGE_PATH"
)
var (
RuntimeStoragePath string
"git.sp4ke.com/sp4ke/bit4sat/config"
"git.sp4ke.com/sp4ke/bit4sat/utils"
)
func GetStoreDestination(filename string) string {
return filepath.Join(RuntimeStoragePath, filename)
return filepath.Join(config.StoragePath, filename)
}
func StoreFormFile(src multipart.File, filename string) error {
@ -68,14 +60,7 @@ func CheckFileExists(file string) (bool, error) {
}
func init() {
path, set := os.LookupEnv(StoragePathEnv)
if !set {
path = StoragePath
}
RuntimeStoragePath = path
err := utils.Mkdir(path)
err := utils.Mkdir(config.StoragePath)
if err != nil {
log.Fatal(err)
}

@ -1,188 +0,0 @@
package storage
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"os"
"git.sp4ke.com/sp4ke/bit4sat-server.git/db"
"git.sp4ke.com/sp4ke/bit4sat-server.git/utils"
"github.com/gin-gonic/gin"
"github.com/segmentio/ksuid"
)
type UploadCtrl struct{}
func (ctrl UploadCtrl) New(c *gin.Context) {
uploadForm := UploadForm{}
if err := c.ShouldBindJSON(&uploadForm); err != nil {
utils.JSONErr(c, http.StatusNotAcceptable,
"could not parse form")
return
}
// Create unique id
id := ksuid.New()
tx, err := db.DB.Handle.Beginx()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
for _, file := range uploadForm.Files {
up := &Upload{}
up.ID = id.String()
log.Println(file.Name)
up.FileName, up.FileExt = utils.CleanFileName(file.Name)
up.FileSize = file.Size
up.FileType = file.Type
up.SHA256 = file.SHA256
up.Status = UpNew
err := up.TxWrite(tx)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
db.RollbackTx(tx, nil)
return
}
}
err = tx.Commit()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{
"status": http.StatusOK,
"result": gin.H{
"id": id,
},
})
}
func (ctrl UploadCtrl) Upload(c *gin.Context) {
// Check that upload ID exists
id := c.Param("id")
exists, err := IdExists(id)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
if !exists {
utils.JSONErr(c, http.StatusNotFound, "id not found")
return
}
form, err := c.MultipartForm()
if err != nil {
utils.JSONErr(c, http.StatusNotAcceptable,
fmt.Sprintf("Could not parse form: %s", err.Error()))
return
}
files := form.File["upload[]"]
tx, err := db.DB.Handle.Beginx()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
for _, file := range files {
log.Printf("Handling %s", file.Filename)
// Get the file's sha256
hasher := sha256.New()
formFd, err := file.Open()
defer formFd.Close()
if err != nil {
utils.JSONErr(
c, http.StatusInternalServerError,
fmt.Sprintf("Could not read file %s", file.Filename),
)
db.RollbackTx(tx, nil)
return
}
_, err = io.Copy(hasher, formFd)
if err != nil {
utils.JSONErr(c, http.StatusInternalServerError,
"")
db.RollbackTx(tx, nil)
return
}
sum256 := hex.EncodeToString(hasher.Sum(nil))
// Get the file's metadata from upload table
up, err := GetByHashID(sum256, id)
if err != nil {
utils.JSONErrPriv(c, http.StatusNotFound, err)
db.RollbackTx(tx, nil)
return
}
log.Printf("Found received file's %s metadata in local database\n", up.FileName)
//log.Println(up)
// Store file
if up.Status == UpStored {
utils.JSONErr(c, http.StatusConflict,
fmt.Sprintf("%s already uploaded", up.FileName))
return
}
//
//
log.Println("Storing file")
err = StoreFormFile(formFd, up.SHA256+up.FileExt)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
db.RollbackTx(tx, nil)
return
}
log.Printf("%s stored at %s", up.FileName,
GetStoreDestination(up.SHA256+up.FileExt))
// Setting status to stored
log.Println("Updating upload status to stored")
up.Status = UpStored
err = up.TxSetState(tx, UpStored)
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
db.RollbackTx(tx, func() {
err := os.Remove(GetStoreDestination(up.SHA256 + up.FileExt))
if err != nil {
log.Println(err)
}
})
return
}
}
err = tx.Commit()
if err != nil {
utils.JSONErrPriv(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{
"status": http.StatusOK,
"result": fmt.Sprintf("%d files uploaded uploaded !", len(files)),
})
}

@ -2,69 +2,218 @@ package storage
import (
"database/sql"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"log"
"math/bits"
"time"
"git.sp4ke.com/sp4ke/bit4sat-server.git/db"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/ln"
"github.com/jmoiron/sqlx"
"github.com/mattn/go-sqlite3"
"github.com/lib/pq"
"github.com/mediocregopher/radix/v3"
)
var DB = db.DB
const (
DBUploadSchema = `
CREATE TABLE IF NOT EXISTS "upload" (
upload_id TEXT NOT NULL,
sha256 TEXT NOT NULL,
file_name TEXT NOT NULL,
file_type TEXT DEFAULT '',
file_size INTEGER NOT NULL,
file_ext TEXT DEFAULT '',
status INTEGER DEFAULT 0,
FOREIGN KEY (status) REFERENCES upload_status(type),
//TODO: sync upload status from redis
// TODO: status is currently handled in cache not here
//
// stored: if file was stored
DBFileUploadsSchema = `
CREATE TABLE IF NOT EXISTS file_uploads (
file_upload_id serial PRIMARY KEY,
upload_id text NOT NULL references uploads(upload_id),
sha256 varchar(64) NOT NULL,
file_name varchar(255) NOT NULL,
file_type varchar(255) DEFAULT '',
file_size integer NOT NULL,
file_ext varchar(255) DEFAULT '',
stored boolean DEFAULT 'false',
UNIQUE (upload_id, sha256)
);
`
DBUploadView = `
CREATE VIEW IF NOT EXISTS "upload_with_status" AS SELECT * FROM upload JOIN upload_status ON
upload.status = upload_status.type
// Unique entry per upload
//
// store: all files for upload where stored
DBUploadsSchema = `
CREATE TABLE IF NOT EXISTS uploads (
upload_id text PRIMARY KEY,
download_id text DEFAULT '',
created timestamp DEFAULT now(),
ask_fee boolean NOT NULL DEFAULT 'false',
ask_currency varchar(3) DEFAULT '',
ask_amount real DEFAULT 0,
status integer NOT NULL DEFAULT 0,
settled boolean DEFAULT 'false',
admin_token text DEFAULT '',
invoice_rhash varchar(64) references invoices(rhash)
);
`
DBUploadStatusSchema = `CREATE TABLE IF NOT EXISTS upload_status
(
type INTEGER PRIMARY KEY,
status TEXT NOT NULL UNIQUE
)
DBUploadPaymentsSchema = `
CREATE TABLE IF NOT EXISTS upload_payments (
payment_id_rhash varchar(64) PRIMARY KEY,
upload_id text NOT NULL references uploads(upload_id),
msat_amount real NOT NULL,
created timestamp DEFAULT now()
);
`
QNewUpload = `INSERT INTO upload
(upload_id, sha256, file_name, file_type, file_size, file_ext, status)
VALUES
(:upload_id, :sha256, :file_name, :file_type, :file_size, :file_ext, :status)`
QSetStatus = `UPDATE upload SET status = :status WHERE upload_id = :upload_id `
//TODO: store all invoices from redis to local sqldb
DBInvoicesSchema = `
CREATE TABLE IF NOT EXISTS invoices (
rhash varchar(64) PRIMARY KEY
);
`
QGetByHashID = `SELECT * FROM upload
WHERE
sha256 = ?
AND
upload_id = ?`
QSetFileStored = `UPDATE file_uploads
SET stored = :stored
WHERE upload_id = :upload_id AND sha256 = :sha256`
QGetByHashID = `SELECT upload_id,
sha256,
file_name,
file_type,
file_size,
file_ext,
stored
FROM file_uploads WHERE sha256 = $1 AND upload_id = $2`
)
type UpStatus uint32
func (st UpStatus) MarshalJSON() ([]byte, error) {
res := map[string]string{}
res["pay_status"] = st.PrintPayStatus()
res["store_status"] = st.PrintStoreStatus()
return json.Marshal(res)
}
func (st *UpStatus) UnmarshalBinary(b []byte) error {
//fmt.Printf("%#v\n", b)
// first 4 bits are reseved
// Single byte will be for 0 value
if len(b) == 1 {
*st = 0
return nil
}
view := binary.BigEndian.Uint32(b)
view = bits.Reverse32(view)
//fmt.Printf("%32b\n", view)
//log.Printf("%d\n", view)
*st = UpStatus(view)
return nil
}
// Get list of positions set
func (st UpStatus) GetFlagPositions() []int {
var i uint32
var setBits []int
for i = 31; i > 0; i-- {
if st&(1<<i) != 0 {
setBits = append(setBits, int(i))
}
}
return setBits
}
func (st UpStatus) PrintStoreStatus() string {
if st.Stored() {
return UploadStatus[UpStored]
} else if st.StoreFail() {
return UploadStatus[WaitStore]
} else {
return UploadStatus[WaitStore]
}
}
func (st UpStatus) PrintPayStatus() string {
if st.Paid() {
return UploadStatus[UpPaid]
}
if st.Expired() {
return UploadStatus[UpPayExpired]
}
return UploadStatus[WaitPay]
}
func (st UpStatus) IsNew() bool {
return (st & UpNew) != 0
}
func (st UpStatus) WaitPay() bool {
return (!st.Paid()) && (!st.Expired())
}
func (st UpStatus) Stored() bool {
return (st & UpStored) != 0
}
func (st UpStatus) StoreFail() bool {
return (st & UpStoreFail) != 0
}
func (st UpStatus) Paid() bool {
return (st & UpPaid) != 0
}
func (st UpStatus) Expired() bool {
return (st & UpPayExpired) != 0
}
func (st UpStatus) GetStoreStatus() UpStatus {
return st & StoreMask
}
func (st UpStatus) GetPayStatus() UpStatus {
return st & PayMask
}
// First 4 bits are reserved for easier parsing from redis
const (
UpNew = iota
UpStored
UpWaitingPayment
UpReady
UpPayExpired UpStatus = 1 << (32 - 1 - iota)
UpPaid
UpStored // All files for this upload where stored
UpStoreFail
// Only used for printing
WaitStore
WaitPay
UpNew = UpStatus(0)
PayMask = UpPaid | UpPayExpired
StoreMask = UpStored
)
var UploadStatus = map[int]string{
UpNew: "new upload",
UpStored: "stored",
UpWaitingPayment: "waiting payment",
UpReady: "ready",
var UploadStatus = map[UpStatus]string{
UpNew: "new upload",
// Payment
UpPayExpired: "expired",
UpPaid: "paid",
WaitPay: "waiting",
// Storage
WaitStore: "waiting storage",
UpStored: "stored",
}
var (
@ -72,35 +221,379 @@ var (
ErrAlreadyExists = errors.New("already exists")
)
type Upload struct {
ID string `db:"upload_id"`
type FileUpload struct {
ID string `db:"file_upload_id"`
UploadId string `db:"upload_id"`
SHA256 string `db:"sha256"`
FileName string `db:"file_name"`
FileType string `db:"file_type"`
FileSize int64 `db:"file_size"`
FileExt string `db:"file_ext"`
Status int `db:"status"`
Stored bool `db:"stored"`
}
// Returns true if id exists in DB
func IdExists(id string) (exists bool, err error) {
qUploadExists := `
SELECT EXISTS (SELECT upload_id FROM upload where upload_id = ?)
type Upload struct {
UploadId string `db:"upload_id"`
DownloadId string `db:"download_id"`
AskFee bool `db:"ask_fee"`
Created time.Time `db:"created"`
AskCurrency string `db:"ask_currency"`
AskAmount float64 `db:"ask_amount"`
InvoiceRhash string `db:"invoice_rhash"` // used as id
Settled bool `db:"settled"`
UploadStatus uint32 `db:"status"` // upload flag status
AdminToken string `db:"admin_token"`
}
type UploadPayments struct {
PaymentId string `db:"payment_id_rhash"`
MsatAmount float64 `db:"msat_amount"`
PayCreated time.Time `db:"payment_created"`
}
type UploadInfo struct {
Upload
UploadPayments
}
func (u Upload) MarshalBinary() ([]byte, error) {
return json.Marshal(u)
}
func (u *Upload) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, &u)
}
func (u FileUpload) MarshalBinary() ([]byte, error) {
return json.Marshal(u)
}
func (u *FileUpload) UnmarshalBinary(b []byte) error {
return json.Unmarshal(b, &u)
}
// TODO: sync from redis to db
//func SyncUploadStatusToDB(){
//}
//
func AddPaymentToUpload(uploadId string, rhash string, msatAmount float64) error {
query := `INSERT INTO
upload_payments (payment_id_rhash, upload_id, msat_amount)
VALUES ($1, $2, $3)
`
err = DB.Handle.Get(&exists, qUploadExists, id)
_, err := DB.Sql.Exec(query, rhash, uploadId, msatAmount)
// No result found is also no result
if err == sql.ErrNoRows {
err = nil
return err
}
func GetDownloadId(uploadId string) (string, error) {
key := fmt.Sprintf("download_for_upload_%s", uploadId)
// Try redis
var downloadId string
var exists bool
// Check if exists
err := DB.Redis.Do(radix.FlatCmd(&exists, "EXISTS", key))
if err != nil {
return "", err
}
//if !exists get from sql and store in redis
if !exists {
log.Printf("redis: dlId %s not found for, trying form db", uploadId)
query := `SELECT download_id from uploads WHERE upload_id = $1`
err = DB.Sql.Get(&downloadId, query, uploadId)
if err != nil {
return "", err
}
if downloadId == "" {
return "", fmt.Errorf("download id missing in %s", uploadId)
}
// Store it back on redis cache
err = db.SetKeyVal(key, downloadId)
} else {
err = db.GetFromKey(key, &downloadId)
}
return downloadId, err
}
func GetUploadIdForDlId(dlId string) (string, error) {
key := fmt.Sprintf("upload_for_download_%s", dlId)
// Try redis
var upId string
var exists bool
// check exists on redis
err := DB.Redis.Do(radix.FlatCmd(&exists, "EXISTS", key))
if err != nil {
return "", err
}
// if not get from sql and store on redis
if !exists {
log.Printf("redis: upId %s not found , trying form db", dlId)
query := `SELECT upload_id from uploads WHERE download_id = $1`
err = DB.Sql.Get(&upId, query, dlId)
if err != nil {
return "", err
}
if len(upId) == 0 {
return "", fmt.Errorf("upload id missing in %s", dlId)
}
// Store it back on redis cache
err = db.SetKeyVal(key, upId)
} else {
err = db.GetFromKey(key, &upId)
}
return upId, err
}
func GetUploadInvoice(uploadId string) (*ln.Invoice, error) {
var invoice ln.Invoice
uploadInvoiceKey := fmt.Sprintf("upload_%s_invoice", uploadId)
err := db.GetFromKey(uploadInvoiceKey, &invoice)
if err != nil {
return nil, err
}
//log.Printf("GetUploadInvoice returned %#v", invoice)
return &invoice, nil
}
func GetUploadFilesMeta(uploadId string) ([]FileUpload, error) {
key := fmt.Sprintf("upload_files_%s", uploadId)
var files []FileUpload
var exists bool
// try redis
err := DB.Redis.Do(radix.FlatCmd(&exists, "EXISTS", key))
if err != nil {
return nil, err
}
// Sql
if !exists {
log.Printf("upload %s files not in cache", uploadId)
query := "SELECT * FROM file_uploads WHERE upload_id = $1"
err := DB.Sql.Select(&files, query, uploadId)
if err != nil {
return nil, err
}
// Store back on redis cache
err = DB.Redis.Do(radix.FlatCmd(nil, "SADD", key, files))
} else {
err = DB.Redis.Do(radix.FlatCmd(&files, "SMEMBERS", key))
}
return files, err
}
// Get all payments linked to an admin token
func GetUploadPaymentsByToken(adminToken string) ([]*UploadInfo, error) {
upInfo := []*UploadInfo{}
query := `SELECT
uploads.upload_id,
uploads.download_id,
uploads.ask_fee,
uploads.ask_currency,
uploads.ask_amount,
uploads.admin_token,
upload_payments.msat_amount,
upload_payments.created AS payment_created
FROM uploads, upload_payments
WHERE uploads.admin_token = $1
`
err := DB.Sql.Select(&upInfo, query, adminToken)
return upInfo, err
}
func GetMsatBalanceByAdminToken(adminToken string) (float64, error) {
var balance float64
query := `SELECT SUM(upload_payments.msat_amount)
FROM uploads, upload_payments
WHERE uploads.upload_id = upload_payments.upload_id
AND uploads.admin_token = $1`
err := DB.Sql.Get(&balance, query, adminToken)
return balance, err
}
func GetUploadById(id string) (*Upload, error) {
key := fmt.Sprintf("upload_%s", id)
up := Upload{}
up.UploadId = id
// Try from redis first
var exists bool
err := DB.Redis.Do(radix.FlatCmd(&exists, "EXISTS", key))
if err != nil {
return nil, err
}
// if does not exists, get it from sql then set it on redis as cache
if !exists {
log.Printf("upload %s not found on redis, caching from sql", id)
query := `SELECT * FROM uploads WHERE upload_id = $1`
err := DB.Sql.Get(&up, query, id)
if err != nil {
return nil, err
}
// Sotre it back on redis
err = db.SetKeyVal(key, up)
// Get it from redis
} else {
err = db.GetFromKey(key, &up)
}
return &up, err
}
func GetUploadIdInvoiceId(uploadId string) (string, error) {
invoice, err := GetUploadInvoice(uploadId)
return invoice.RHash, err
}
func SetUploadStatus(id string, status UpStatus) error {
//log.Printf("setting upload status for %s", id)
key := fmt.Sprintf("upload_status_%s", id)
if status == UpNew {
return DB.Redis.Do(radix.FlatCmd(nil, "SETBIT", key, 31, 0))
}
//log.Println("setting upload status for bit positions ", status.GetFlagPositions())
// get bit positions
for _, offset := range status.GetFlagPositions() {
//log.Printf("setting bit at position %d", offset)
err := DB.Redis.Do(radix.FlatCmd(nil,
"SETBIT", key, offset, 1))
if err != nil {
return err
}
}
// Store on sql db
query := `UPDATE uploads SET status = $1 WHERE upload_id = $2`
_, err := DB.Sql.Exec(query, uint32(status), id)
if err != nil {
return err
}
return nil
}
func GetUploadStatus(id string) (status UpStatus, err error) {
//log.Println("Getting upload status")
key := fmt.Sprintf("upload_status_%s", id)
err = db.GetFromKey(key, &status)
return
}
func SetUploadAdminToken(uploadId, token string) error {
key := fmt.Sprintf("upload_admin_%s", uploadId)
err := db.SetKeyVal(key, token)
if err != nil {
return err
}
query := `UPDATE uploads SET admin_token = $1 WHERE upload_id = $2`
_, err = DB.Sql.Exec(query, token, uploadId)
return err
}
func GetUploadAdminToken(uploadId string) (string, error) {
var token string
key := fmt.Sprintf("upload_admin_%s", uploadId)
err := db.GetFromKey(key, &token)
return token, err
}
func SetUploadInvoice(uploadId string, invoice *ln.Invoice) error {
uploadInvoiceKey := fmt.Sprintf("upload_%s_invoice", uploadId)
err := db.SetKeyVal(uploadInvoiceKey, invoice)
if err != nil {
return err
}
// Set inverse relation
invoiceUploadKey := fmt.Sprintf("invoice_%s_upload_id", invoice.RHash)
err = db.SetKeyVal(invoiceUploadKey, uploadId)
if err != nil {
return err
}
// SQL insert
tx, err := DB.Sql.Beginx()
if err != nil {
return err
}
// Add invoice to invoices table
query := `INSERT INTO invoices(rhash)
VALUES($1)
ON CONFLICT DO NOTHING
`
_, err = tx.Exec(query, invoice.RHash)
if err != nil {
return err
}
// Set invoice to upload
query = `UPDATE uploads SET invoice_rhash = $1 WHERE upload_id = $2`
_, err = tx.Exec(query, invoice.RHash, uploadId)
if err != nil {
return err
}
return tx.Commit()
}
func GetUploadIdForInvoice(invoiceId string) (string, error) {
key := fmt.Sprintf("invoice_%s_upload_id", invoiceId)
err := db.GetFromKey(key, &invoiceId)
return invoiceId, err
}
// Returns true if id exists in DB
func IdExists(id string) (exists bool, err error) {
key := fmt.Sprintf("upload_status_%s", id)
err = DB.Redis.Do(radix.Cmd(&exists, "EXISTS", key))
return
}
// Get a file by upload id and hash
func GetByHashID(sha256 string, id string) (*Upload, error) {
var up Upload
err := DB.Handle.Get(&up, QGetByHashID, sha256, id)
func GetByHashID(sha256 string, id string) (*FileUpload, error) {
var up FileUpload
err := DB.Sql.Get(&up, QGetByHashID, sha256, id)
if err == sql.ErrNoRows {
return nil, ErrDoesNotExist
@ -113,8 +606,32 @@ func GetByHashID(sha256 string, id string) (*Upload, error) {
return &up, nil
}
func (u *Upload) TxSetState(tx *sqlx.Tx, status int) error {
_, err := tx.NamedExec(QSetStatus, u)
func (u *FileUpload) TxSetFileStored(tx *sqlx.Tx) error {
u.Stored = true
_, err := tx.NamedExec(QSetFileStored, u)
if err != nil {
return err
}
return nil
}
func (u *FileUpload) TxWrite(tx *sqlx.Tx) error {
query := `INSERT INTO file_uploads
(upload_id, sha256, file_name, file_type, file_size, file_ext)
VALUES
(:upload_id, :sha256, :file_name, :file_type, :file_size, :file_ext)
`
_, err := tx.NamedExec(query, u)
if pqError, ok := err.(*pq.Error); ok {
// unique constraint
if pqError.Code == "23505" {
return ErrAlreadyExists
}
}
if err != nil {
return err
@ -124,11 +641,18 @@ func (u *Upload) TxSetState(tx *sqlx.Tx, status int) error {
}
func (u *Upload) TxWrite(tx *sqlx.Tx) error {
query := `INSERT INTO uploads
(upload_id, download_id, ask_fee, ask_currency, ask_amount, settled)
VALUES
(:upload_id, :download_id, :ask_fee, :ask_currency, :ask_amount, :settled)`
_, err := tx.NamedExec(QNewUpload, u)
sqlErr, isSqlErr := err.(sqlite3.Error)
if isSqlErr && sqlErr.Code == sqlite3.ErrConstraint {
return ErrAlreadyExists
_, err := tx.NamedExec(query, u)
if pqError, ok := err.(*pq.Error); ok {
// unique constraint
if pqError.Code == "23505" {
return ErrAlreadyExists
}
}
if err != nil {
@ -139,10 +663,17 @@ func (u *Upload) TxWrite(tx *sqlx.Tx) error {
}
func (u *Upload) Write() error {
_, err := DB.Handle.NamedExec(QNewUpload, u)
sqlErr, isSqlErr := err.(sqlite3.Error)
if isSqlErr && sqlErr.Code == sqlite3.ErrConstraint {
return ErrAlreadyExists
query := `INSERT INTO uploads
(upload_id, download_id, ask_fee, ask_currency, settled)
VALUES
(:upload_id, :download_id, :ask_fee, :ask_currency, :settled)`
_, err := DB.Sql.NamedExec(query, u)
if pqError, ok := err.(*pq.Error); ok {
// unique constraint
if pqError.Code == "23505" {
return ErrAlreadyExists
}
}
if err != nil {
@ -153,35 +684,24 @@ func (u *Upload) Write() error {
}
func init() {
_, err := DB.Handle.Exec(DBUploadSchema)
_, err := DB.Sql.Exec(DBInvoicesSchema)
if err != nil {
log.Fatal(err)
}
_, err = DB.Handle.Exec(DBUploadView)
_, err = DB.Sql.Exec(DBUploadsSchema)
if err != nil {
log.Fatal(err)
}
_, err = DB.Handle.Exec(DBUploadStatusSchema)
_, err = DB.Sql.Exec(DBFileUploadsSchema)
if err != nil {
log.Fatal(err)
}
// Populate status types
query := `INSERT INTO upload_status (type, status) VALUES(?,?)`
for k, v := range UploadStatus {
_, err := DB.Handle.Exec(query, k, v)
if err != nil {
sqlErr, ok := err.(sqlite3.Error)
if ok && sqlErr.ExtendedCode == sqlite3.ErrConstraintUnique {
log.Panic(err)
}
if !ok {
log.Panic(err)
}
}
_, err = DB.Sql.Exec(DBUploadPaymentsSchema)
if err != nil {
log.Fatal(err)
}
}

@ -0,0 +1,57 @@
package storage
import (
"archive/zip"
"io"
"os"
)
func ZipFiles(files []FileUpload, w io.Writer) error {
zipWriter := zip.NewWriter(w)
defer zipWriter.Close()
// Add files to zip
for _, file := range files {
if err := AddFileToZip(zipWriter, file); err != nil {
return err
}
}
return nil
}
// https://golangcode.com/create-zip-files-in-go/
func AddFileToZip(zipWriter *zip.Writer, file FileUpload) error {
fileToZip, err := os.Open(GetStoreDestination(file.SHA256 + file.FileExt))
if err != nil {
return err
}
defer fileToZip.Close()
// get file info
info, err := fileToZip.Stat()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// Reset original file name
header.Name = file.FileName
// Change to deflate for better compression
header.Method = zip.Deflate
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = io.Copy(writer, fileToZip)
return nil
}

@ -15,5 +15,5 @@ func JSONErr(c *gin.Context, status int, msg string) {
func JSONErrPriv(c *gin.Context, status int, err error) {
log.Println(err)
JSONErr(c, status, "")
JSONErr(c, status, "server error")
}

@ -0,0 +1,22 @@
package utils
import (
"net"
"net/http"
"time"
)
func NewHttpClient() *http.Client {
netTransport := &http.Transport{
Dial: (&net.Dialer{
Timeout: 5 * time.Second,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
c := &http.Client{
Timeout: time.Second * 10,
Transport: netTransport,
}
return c
}

@ -0,0 +1,197 @@
package watchers
import (
"encoding/hex"
"fmt"
"io"
"log"
"regexp"
"git.sp4ke.com/sp4ke/bit4sat/bus"
"git.sp4ke.com/sp4ke/bit4sat/db"
"git.sp4ke.com/sp4ke/bit4sat/ln"
"git.sp4ke.com/sp4ke/bit4sat/lndrpc"
"git.sp4ke.com/sp4ke/bit4sat/storage"
lnrpc "github.com/lightningnetwork/lnd/lnrpc"
"github.com/mediocregopher/radix/v3"
"golang.org/x/net/context"
)
const (
LastSettledInvoiceIndexKey = "last_invoice_settled_index_cursor"
ReUploadInvoice = `bit4sat upload`
ReDownInvoice = `bit4sat download`
)
// Last watched invoice
func getLastSettledInvoiceIndex() uint64 {
var cursor uint64
err := db.DB.Redis.Do(radix.FlatCmd(&cursor, "GET", LastSettledInvoiceIndexKey))
if err != nil {
return uint64(0)
}
return cursor
}
func setLastSettledInvoiceIndex(index uint64) {
err := db.DB.Redis.Do(radix.FlatCmd(nil, "SET", LastSettledInvoiceIndexKey, index))
if err != nil {
panic(err)
}
}
// Runs in a goroutine and keep swatching invoices
func WatchInvoice() {
// Get the last invoice index cursor
cursor := getLastSettledInvoiceIndex()
log.Printf("watching settled from index %d", cursor)
ctxb := context.Background()
client, cleanUp := lndrpc.GetClient()
defer cleanUp()
subscription := &lnrpc.InvoiceSubscription{
SettleIndex: cursor,
}
invoiceStream, err := client.SubscribeInvoices(ctxb, subscription)
if err != nil {
//return nil, err
}
for {
invoice, err := invoiceStream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Printf("error invoice stream %s", err)
break
}
log.Printf("watcher, received invoice \n%s\n", invoice)
// We are only interested in settled invoices
if !invoice.Settled {
continue
}
// Invoice was paid, we handle it
handleSettledInvoice(invoice)
}
}
func handleSettledInvoice(invoice *lnrpc.Invoice) {
// we need to save it and also notify the pubsub channel
// Save last settled index
setLastSettledInvoiceIndex(invoice.SettleIndex)
matchUp, err := regexp.MatchString(ReUploadInvoice, invoice.Memo)
if err != nil {
log.Printf("Error regex match: %s", err)
}
matchDown, err := regexp.MatchString(ReDownInvoice, invoice.Memo)
if err != nil {
log.Printf("Error regex match: %s", err)
}
// Handle upload related invoices
if matchUp {
// Update upload status to "paid"
uploadId, err := storage.GetUploadIdForInvoice(hex.EncodeToString(invoice.RHash))
log.Printf("watcher: found upload id %s for invoice %s", uploadId,
hex.EncodeToString(invoice.RHash))
if err != nil {
log.Printf("error handleSettledInvoice: %s", err)
return
}
err = storage.SetUploadStatus(uploadId, storage.UpPaid)
if err != nil {
log.Printf("error handleSettledInvoice: %s", err)
return
}
// Get stored invoice for upload
storedInvoice, err := storage.GetUploadInvoice(uploadId)
if err != nil {
log.Printf("error handleSettledInvoice: %s", err)
return
}
//log.Printf("stored invoice %#v", storedInvoice)
// Update stored invoice fields
newInvoice := ln.UpdateInvoiceFromLnd(storedInvoice, invoice)
//log.Printf("new invoice %#v", newInvoice)
// Set invoice for upload
err = storage.SetUploadInvoice(uploadId, newInvoice)
if err != nil {
log.Printf("error handleSettledInvoice: %s", err)
return
}
log.Printf("Notifying invoice paid for upload %s", uploadId)
// publish invoice was updated to upload_id_paid channel
key := fmt.Sprintf("%s:%s", bus.InvoicePaidChannelPrefix,
newInvoice.RHash)
err = db.DB.Redis.Do(radix.FlatCmd(nil, "PUBLISH",
key, newInvoice))
}
// This is a download invoice
if matchDown {
// Store the full invoice
key := fmt.Sprintf("invoice_%s", hex.EncodeToString(invoice.RHash))
storedInvoice := ln.InvoiceFromLndIn(invoice)
err = db.DB.Redis.Do(radix.FlatCmd(nil, "SET", key, storedInvoice))
if err != nil {
log.Printf("error handleSettledInvoice: %s", err)
return
}
// Get upload_id related to this invoice
key = fmt.Sprintf("invoice_id_%s_upload_id", storedInvoice.RHash)
//log.Printf("looking for %s", key)
var uploadId string
err := db.DB.Redis.Do(radix.FlatCmd(&uploadId, "GET", key))
if err != nil {
log.Printf("error handleSettledInvoice: %s", err)
return
}
// Add payment to the corresponding upload_id (user account)
//log.Printf("updating payment for upload %s", uploadId)
err = storage.AddPaymentToUpload(uploadId, storedInvoice.RHash,
storedInvoice.Msatoshi)
if err != nil {
log.Printf("error handleSettledInvoice: %s", err)
return
}
log.Printf("Notifying invoice paid for download")
key = fmt.Sprintf("%s:%s", bus.InvoicePaidChannelPrefix,
storedInvoice.RHash)
err = db.DB.Redis.Do(radix.FlatCmd(nil, "PUBLISH",
key, storedInvoice))
}
if err != nil {
log.Printf("error handleSettledInvoice: %s", err)
return
}
}

@ -0,0 +1,42 @@
0.0.0.0
root dist
ext .html
header / {
Content-Security-Policy "default-src * 'unsafe-eval' 'unsafe-inline' data: ;"
}
## API
proxy /api localhost:8880 {
transparent
}
## Download rewrite
rewrite /d {
to /
}
## Redeem view
rewrite /r {
to /
}
## test
proxy /t localhost:8880 {
transparent
}
#log /api stdout
log / stdout
#proxy /ws localhost:8880 {
# websocket
# transparent
#}
#tls self_signed

@ -0,0 +1,31 @@
#172.29.195.31
0.0.0.0
root /srv
ext .html
header / {
Content-Security-Policy "default-src * 'unsafe-eval' 'unsafe-inline' data: ;"
}
proxy /api bit4sat-api:8880 {
transparent
}
## Download rewrite
rewrite /d {
to /
}
#log /api stdout
log / stdout
#proxy /ws localhost:8880 {
# websocket
# transparent
#}
#tls self_signed

Binary file not shown.

@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width">
<title></title>
</head>
<body>
<body class="sans-serif bg-white">
<div id="app">
</div>

@ -18,13 +18,19 @@
},
"dependencies": {
"babel-polyfill": "^6.26.0",
"qrcode": "^1.3.3",
"tachyons": "4.10.0",
"vue": "^2.6.8",
"vue-hot-reload-api": "^2.3.3"
"vue-hot-reload-api": "^2.3.3",
"vue-router": "^3.0.2",
"vuex": "^3.1.0"
},
"alias": {
"vue": "./node_modules/vue/dist/vue.common.js"
},
"scripts": {
"start": "parcel index.html"
"start": "parcel index.html",
"watch": "parcel watch index.html",
"build": "parcel build index.html"
}
}

@ -0,0 +1,32 @@
<template>
<div id="admin">
<p v-show="apiDone">Current balance: {{balance}}</p>
<textarea name="payreqRedeem"></textarea>
<button name="redeem">Redeem</button>
</div>
</template>
<script>
import Api from './api.js'
export default {
name: 'AdminView',
data(){
return {
balance: 0,
apiDone: false,
}
},
props: ['adminToken'],
created(){
let self = this
Api.adminInfo(this.adminToken)
.then((res)=>{
res.json().then((data)=>{
self.balance = data.msat_avail
self.apiDone = true
})
})
}
}
</script>

@ -1,21 +1,80 @@
<template>
<div id="app">
<upload></upload>
<div id="app" class="flex flex-column items-center">
<div id="logo" class="w-100 flex justify-center items-end">
<a href="/" class="logo-titla f1 lh-title tracked">BIT4SAT</a>
</div>
<div class="main h-100 w-100 flex mt4 items-start justify-center">
<router-view />
</div>
</div>
</template>
<script charset="utf-8">
import Upload from './upload.vue';
import GetWorker from './workerInterface.js';
const Worker = GetWorker('main');
import { mapState, mapGetters } from 'vuex'
const dlUrlRegex = /d\/(\w+)\/?/
const adminUrlRegex = /r\/(\w+)\/?/
export default {
name: 'app',
data() {
return {
msg: 'Hello World'
mounted(){
this.worker = Worker
// listen and route
this.worker.listenTo('upload-invoice', (e) => {
console.log("received invoice ", e.data)
this.$store.commit('setInvoice', e.data.invoice)
this.$store.commit('setUpStatus', e.data.status)
// if we are not on upload route go there
if ( this.$router.currentRoute.name !== 'upload' ) {
this.$router.push({name: 'upload', params: { uploadId: e.data.upload_id }})
}
})
// On load if the url is a download url route to download path
let loc = window.location
if (loc.pathname.match(dlUrlRegex)) {
let dlId = dlUrlRegex.exec(loc.pathname)[1]
history.replaceState("", `download ${dlId}`, '/' )
this.$router.replace({
name: 'download',
replace: true,
params: {dlId}
})
}
if (loc.pathname.match(adminUrlRegex)) {
let adminToken = adminUrlRegex.exec(loc.pathname)[1]
history.replaceState("", `admin`, '/' )
this.$router.replace({
name: 'admin',
replace: true,
params: {adminToken}
})
}
},
components: {
Upload,
}
}
</script>
<style>
#app {
height: 100vh;
}
#logo {
height: 25%;
}
#logo a {
color: #FF725C;
font-style: italic;
font-weight: bolder;
text-shadow: 2px 3px #444, -2px -2px #444;
text-decoration: none;
}
</style>

@ -0,0 +1,109 @@
<template>
<div id="download">
<pay v-if="!error && !showDl" :objectId="dlId" :invoice="invoice"></pay>
<div v-if="error" class="f4 mv5 light-red ttu">{{errorMsg}}</div>
<upload v-if="error"></upload>
<a v-if="showDl" :href="dlLink" download="download.zip">download file</a>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import Pay from './Pay.vue';
import Api from './api.js';
import Upload from './Upload.vue';
export default {
name: 'DownloadView',
props: ['dlId'],
data(){
return {
dlLink: "if",
showDl: false,
error: false,
errorMsg: "",
}
},
created(){
let self = this
// Query for download invoice
Api.download(this.dlId)
.then((res)=>{
if (res.status === 404){
self.error = true
self.errorMsg = "this link does not exist anymore"
// ask payment
} else if ( res.status === 402) {
return res.json()
.then((data)=>{
this.$store.commit('setInvoice', data.invoice)
this.$store.commit('setFiles', data.files)
Api.pollInvoice(data.invoice.rhash)
.then((res)=>{
// invoice paid we can try again to download
if(res.ok){
self.fetchDownloadKey()
}
})
}) .catch((e)=>{console.log(e)})
} else {
console.log("calling set download")
res.json().then((data)=>{
console.log(data)
self.setDownload(data.download_key)
})
}
})
},
methods: {
fetchDownloadKey() {
Api.download(this.dlId)
.then((res)=>{
if (!res.ok ) {
this.error = true
this.errMsg = "an error occured, try reloading the page"
} else {
res.json().then((data)=>{
this.setDownload(data.download_key)
})
}
})
},
setDownload(downloadKey){
this.showDl = true;
this.dlLink = Api.endPoints.getFiles + '/' + downloadKey
}
},
computed: {
...mapState({
invoice: state => state.base.invoice,
files: state => state.down.files,
}),
...mapGetters([
'paid',
'unpaid'
]),
downloadLink(){
return 'TODO'
},
},
components: {
Pay,
Upload,
}
}
</script>

@ -0,0 +1,77 @@
<template>
<div id="home" class="flex flex-column justify-center items-center">
<upload></upload>
<div class="desc f6 pt2 b lh-copy measure-narrow gray tc">
<p>
If choose to setup a fee for your link, every download will
require a payment.
</p>
<p>You can redeem your payments at any time using your admin token.</p>
<p class="red b f5">WARNING: this is beta software running on mainnet. Do not send
more than 1 satoshi for testing. The admin token access is still
a work in progress.</p>
</div>
<!--<pay :uploadId="uploadId" :status="status" :invoice="invoice"></pay>-->
<!--<download-link v-if="uploadId && paid" :id="uploadId"></download-link>-->
</div>
</template>
<script charset="utf-8">
import Upload from './Upload.vue';
import GetWorker from './workerInterface.js';
import { lastSession } from './api.js';
const Worker = GetWorker('main');
export default {
name: 'app',
props: ['uploadId', 'invoice', 'status'],
mounted (){
let self = this;
this.worker = Worker;
// listen to worker events
// this.worker.listenTo('upload-invoice', (e) => {
// console.log("received invoice ", e.data)
// self.invoice = e.data.invoice;
// self.uploadId = e.data.upload_id;
// self.status = e.data.status;
//
// })
//
// this.worker.listenTo('payment-received', (e)=>{
// console.log('pay received', e.data)
// this.invoice = e.data.invoice;
// this.status = e.data.status;
// })
// Get last session uploadId if it exists
lastSession()
.then((data) => {
// If upload id is unpaid
if (data.uploadId !== 0){
// Send to upload view
this.$store.commit('setInvoice', data.invoice)
this.$store.commit('setUpStatus', data.status)
console.log("push to upload view ", data.uploadId)
this.$router.push({name: 'upload',
params: { uploadId: data.upload_id }
})
}
})
},
components: {
Upload,
}
}
</script>
<style>
</style>

@ -0,0 +1,143 @@
<template>
<div id="payment" class="flex flex-column">
<div class="header flex flex-column justify-between items-start mb1">
<div class="uid flex justify-between items-center w-100 f5">
<span class="b f5 mid-gray">ID: {{objectId}}</span>
</div>
</div>
<img class="ba bw3 light-gray" v-if="unpaid" :src="payreqURI" />
<div v-if="showTimer" class="payreq-wrapper bg-washed-yellow flex justify-center">
<div class="payreq b f6 tc courier gray">
<p id="payreq" @click="selectCopy">
{{invoice.payreq}}
</p>
</div>
</div>
<div class="status flex flex-row justify-between items-center">
<div class="paid">
<p class="light-red f5 mv2" v-if="unpaid">UNPAID</p>
<p class="yellow b f5 mv2" v-if="expired">EXPIRED</p>
<p class="green b f5 mv2" v-if="paid">PAID</p>
</div>
<timer v-if="showTimer" :expires="expires" :expired="expired"></timer>
<div class="paid-date f6 mid-gray" v-if="paid">on &nbsp;{{paidAt}}</div>
</div>
<span v-if="unpaid" class="b f5 mid-gray">{{invoice.msatoshi / 1000}} Satoshi</span>
<canvas id="canvas" hidden>
</div>
</template>
<script charset="utf-8">
import { mapState, mapGetters } from 'vuex'
import QRCode from 'qrcode';
import Timer from './Timer.vue';
export default {
data() {
return {
payreqURI: "",
expired: false,
expires: 0,
}
},
props: ['invoice', 'objectId'],
methods:{
makeLnQR(payreq) {
let canvas = this.$el.querySelector('#canvas')
QRCode.toDataURL(canvas, payreq.toUpperCase(), {
margin: 4,
width: 340,
errorCorrectionLevel: 'H',
type: 'png',
color: {
dark: '#ff725cff',
},
renderOpts:{
quality: 1,
}
})
.then(url => {
this.payreqURI = url;
})
.catch(err =>{
console.error(err);
})
},
selectCopy(ev){
//ev.target.select()
//document.execCommand('copy')
console.log("TODO: select and copy to clipboard")
}
},
computed: {
...mapGetters([
'paid',
'unpaid'
]),
paidAt: function(){
return new Date(this.invoice.paid_at).toGMTString();
},
showTimer: function(){
return !this.paid
}
},
watch: {
invoice: function(val) {
if (val){
console.log("new invoice")
this.expires = 0;
this.expires = val.expires_at;
if ((new Date(val.expires_at*1000) - new Date() <= 0 ) &&
!this.paid) {
this.expired = true;
} else {
this.expired = false;
}
this.makeLnQR(val.payreq)
}
}
},
components:{
Timer,
}
}
</script>
<style>
img {
width: var(--qrcode-width);
height: var(--qrcode-width);
}
.status {
min-width: var(--qrcode-width);
}
.payreq {
word-wrap: break-word;
max-width: var(--qrcode-width);
}
</style>

@ -0,0 +1,63 @@
export default class Timer {
// Recevies a date in the future
constructor(expiresAt){
let self = this;
this.secsLeft = 0;
this.minsLeft = 0;
this.hoursLeft= 0;
this.done = false;
// Convert epoch timestamp to date
if (typeof(expiresAt) === 'number'){
this.deadline = new Date(expiresAt);
} else if (expiresAt instanceof Date){
this.deadline = expiresAt
} else {
throw("need instance of Date or unix timestamp")
}
// Timer done
if (this.deadline - new Date() <= 0) {
this.done = true;
} else {
this.secsLeft = new Date(this.deadline - new Date()).getSeconds();
this.minsLeft = new Date(this.deadline - new Date()).getMinutes();
this.hoursLeft = new Date(this.deadline - new Date()).getHours();
// Run ticker
this.interval = setInterval(function(){
self.tick()
}, 1000)
}
}
get secs () {
return this.secsLeft;
}
get mins(){
return this.minsLeft;
}
get hours(){
return this.hoursLeft;
}
tick() {
if (this.deadline - new Date() <= 0) {
this.done;
clearInterval(this.interval);
return
}
this.secsLeft = new Date(this.deadline - new Date()).getSeconds();
this.minsLeft = new Date(this.deadline - new Date()).getMinutes();
this.hoursLeft = new Date(this.deadline - new Date()).getHours();
}
}

@ -0,0 +1,54 @@
<template>
<div class="timer">
<p>remaining: {{mins}}:{{secs}}</p>
</div>
</template>
<script>
import Timer from './Timer.js'
export default {
data(){
return {
timer: null,
}
},
methods: {
restart(){
console.log('restart timer')
this.timer = new Timer(new Date(this.expires*1000));
}
},
props: ['expires', 'expired'],
computed: {
secs: function() {
if (this.timer){
return this.timer.secs;
}
},
mins: function(){
if (this.timer){
return this.timer.mins;
}
}
},
watch: {
expires:{
immediate: true,
handler(val, oldVal){
let self = this;
this.timer = new Timer(new Date(val*1000));
}
}
}
}
</script>
<style>
p.expired {
color: red;
}
</style>

@ -0,0 +1,146 @@
<template>
<div @dragenter.stop.prevent @dragover.stop.prevent @drop.stop.prevent="drop" id="upload" class="flex flex-column ph4 pv4 ba bw1 b--light-red br1 bg-near-white mid-gray">
<div class="title f6 b lh-title ttu tc">drop files here</div>
<div class="or f6 mv2 tc">or</div>
<form class="input-reset flex flex-column justify-center">
<!--hidden file input-->
<input id="fileInput" class="dn" v-on:change="fileChange($event)" type="file" multiple />
<button class="mb3" @click.stop.prevent="chooseFiles" id="fileSelect">pick files</button>
<div class="askFee flex justify-start items-center">
<input id="request_payment" type="checkbox" v-model="requestPay">
<label class="ph2 pv2" for="request_payment">Ask fee for download ?</label>
</div>
<div class="options w-100 flex flex-row justify-between">
<input :disabled="!options.request_payment" placeholder="ask fee for download" v-model.trim.number="payAmount" type="number">
<select :disabled="!options.request_payment" v-model="payCurrency">
<option v-for="cur in currencies">{{cur}}</option>
</select>
</div>
<!--<p>selected {{fileCount}} files</p>-->
<!--<ul>-->
<!--<li v-bind:files="files" v-for="file in files"> -->
<!--{{ file.name }}-->
<!--<ul>-->
<!--<li>size: {{ file.size / 1000 }} kB </li>-->
<!--<li>type: {{ file.type }} </li>-->
<!--</ul>-->
<!--</li>-->
<!--</ul>-->
</form>
</div>
</template>
<script charset="utf-8">
import { mapState } from 'vuex'
import GetWorker from './workerInterface.js';
const Worker = GetWorker('main');
export default {
data(){
return {
fileCount: 0,
files: [],
currencies: ['EUR', 'USD', 'SAT', 'MSAT'],
}
},
mounted(){
this.fileInput = this.$el.querySelector('#fileInput')
this.worker = Worker
},
computed:{
...mapState({
options: state => state.upload.options
}),
payAmount: {
get(){
return this.$store.state.upload.options.request_payment_amount
},
set (value) {
this.$store.commit('setPayAmount', value)
}
},
payCurrency: {
get(){
return this.$store.state.upload.options.payment_currency
},
set(val){
this.$store.commit('setCurrency', val)
}
},
requestPay: {
get(){
return this.$store.state.upload.options.request_payment
},
set(val){
return this.$store.commit('setRequestPay', val)
}
}
},
methods: {
drop(event){
this.files=[]
const dt = event.dataTransfer;
this.fileCount = dt.files.length;
this.files = dt.files;
this.worker.post({
msg: 'new-upload',
payload: {
files: [...this.files],
options: this.options
}
})
},
chooseFiles(event){
console.log("calling choose files")
this.fileInput.click();
},
fileChange(event) {
// reset
this.files = []
this.fileCount = event.target.files.length
this.files = event.target.files
console.log('filechange notifying new-upload')
this.worker.post({
msg: 'new-upload',
payload: {
files: [...this.files],
options: this.options
}
})
// Get sha256 of files
// for (let file of this.files) {
// w.post({
// msg: 'file-sha256',
// payload: file
// })
// }
}
}
}
</script>

@ -0,0 +1,141 @@
<template>
<div id="upload-view" class="flex flex-column items-center justify-center">
<p v-show="!paid && !expired" class="dn f6 mb4 mid-gray">To avoid spam you are asked to make a one-time payment equivalent to the
fee you ask for your link</p>
<pay v-show="!paid" :objectId="uploadId" :invoice="invoice"></pay>
<div class="hr"></div>
<form id="accepted" class="flex flex-column mt5 w-100" v-if="accepted" >
<label for="adminToken" class="f7 db mb2">
<span class="normal b orange">Your admin token:</span>
</label>
<input class="input-reset f6 b ba b--black-20 pa2 mb2 db w-100 mid-gray" @click="selectCopy" id="adminToken" type="text" v-model="adminToken">
<label for="downloadLink" class="f7 db mb2 mt3">
<span class="normal b blue">Download link:</span>
</label>
<input class="input-reset f6 b ba b--black-20 pa2 mb2 db w-100 mid-gray" @click="selectCopy" id="downloadLink" type="text" v-model="downloadLink">
</form>
</div>
</template>
<script charset="utf-8">
import { mapState, mapGetters } from 'vuex'
import Upload from './Upload.vue';
import Pay from './Pay.vue';
import GetWorker from './workerInterface.js';
import Api from './api.js';
const Worker = GetWorker('main');
export default {
name: 'UploadView',
data() {
return {
accepted: false,
adminToken: "",
downloadId: "",
}
},
props: ['uploadId'],
created (){
let self = this;
this.worker = Worker;
// First check the status to get the invoice
Api.checkUploadStatus(this.uploadId)
.then((res)=>{
return res.json()
.then((data)=>{
this.$store.commit('setInvoice', data.invoice)
// Set upload metadata
this.$store.commit('setUpStatus', data.status)
return data
})
})
.catch((err)=>{
console.error(err)
})
.then((data)=>{
// if payment required, poll for invoice paid
if (data.status.pay_status === 'waiting') {
Api.pollUploadStatus(self.uploadId)
.then((res)=>{ return res.json() })
.then((data)=>{
console.log(data)
self.$store.commit('setInvoice', data.invoice)
self.$store.commit('setUpStatus', data.status)
// if paid we get the admin/dl link
if (data.status.pay_status == 'paid' ){
self.accepted = true;
({ admin_token: self.adminToken,
download_id: self.downloadId } = data);
}
})
}
})
},
methods:{
selectCopy(ev){
ev.target.select()
document.execCommand('copy');
}
},
computed: {
...mapState({
status: state => state.upload.status,
invoice: state => state.base.invoice,
}),
...mapGetters([
'paid',
'expired',
]),
downloadLink(){
let loc = window.location;
return loc.host + '/d/' + this.downloadId
},
},
components: {
Upload,
Pay,
}
}
</script>
<style>
#upload-view{
min-width: var(--qrcode-width);
}
#accepted input:focus {
background: #fffceb;
}
input#adminToken::selection {
color: #ff6300;
}
input#downloadLink::selection {
color: #357Edd;;
}
</style>

@ -0,0 +1,73 @@
import { apiPort } from './api.js';
let singleton = false;
export class WS {
constructor(){
console.log("starting new websocket")
if (singleton && singleton.open) {
return singleton;
}
const endpoint = `ws://localhost:2015/ws`;
this.conn = new WebSocket(endpoint);
this.conn.onerror = this.onerror();
this.conn.onclose = this.onclose();
this.conn.onmessage = this.onmessage();
this.conn.onopen = this.onopen();
}
send(data) {
this.conn.send(data);
}
onopen(){
let self = this;
return (ev) => {
self.opened = true;
console.log("websocket opened");
}
}
onerror (){
let self = this;
return (ev) => {
console.error(ev);
}
}
onclose () {
let self = this;
let instance = singleton
return (ev) => {
console.warn("websocket closed");
//TODO: restore on prod
//setTimeout(() =>{
//console.log("websocket: trying to reconnect")
//instance = new WS()
//}, 3000)
}
}
onmessage() {
let self = this;
return (ev) => {
let msg = JSON.parse(ev.data);
switch (msg.type) {
case 'hello':
console.log(msg.data);
break;
case 'upload-accepted':
console.log(msg)
break;
}
}
}
}

@ -1,36 +1,98 @@
import 'babel-polyfill';
let apiPort = '8880'
if (process.env.API_PORT !== undefined) {
apiPort = process.env.API_PORT;
}
function getEndpoint (path) {
return new URL(path, apiEndpoint).toString()
}
const endPoints = {
get upload () {
return getEndpoint('upload/')
}
upload: '/api/v1/u',
session: '/api/v1/session',
pollupstatus: '/api/v1/u/poll',
checkstatus: '/api/v1/u/check',
download: '/api/v1/d/q', // query download
getFiles: '/api/v1/d/g', // get file
pollinvoice: '/api/v1/pollinvoice',
adminToken: '/api/v1/a/info'
}
export function adminInfo(adminToken){
let req = new Request(endPoints.adminToken + '/' + adminToken,{
method: 'GET',
credentials: 'same-origin'
})
console.log(req)
return fetch(req).catch((e)=>{console.error(e)})
}
export async function download(dlId){
let req = new Request(endPoints.download + '/' + dlId,{
method: 'GET',
credentials: 'same-origin'
})
return fetch(req).catch((e)=>{console.error(e)})
}
export async function lastSession(){
let req = new Request(endPoints.session, {
method: 'GET',
credentials: 'same-origin'
})
let res = await fetch(req)
.catch((e) => {console.error(e)})
return res.json()
}
export async function checkUploadStatus(uploadId){
let req = new Request(endPoints.checkstatus + '/' + uploadId,{
method: 'GET',
credentials: 'same-origin'
})
return fetch(req).catch((e)=>{console.error(e)})
}
export function pollInvoice(invoiceId){
let req = new Request(endPoints.pollinvoice + '/' + invoiceId,{
method: 'GET',
credentials: 'same-origin'
})
return fetch(req).catch((e)=>{console.error(e)})
}
const apiEndpoint = function() {
let currentLoc = self.location;
let endpoint = new URL(currentLoc);
export async function pollUploadStatus(uploadId){
//console.log('polling upload status')
let req = new Request(endPoints.pollupstatus + '/' + uploadId,{
method: 'GET',
credentials: 'same-origin'
})
endpoint.port = apiPort;
return fetch(req).catch((e)=>{console.error(e)})
return endpoint.toString()
}()
}
class Upload {
constructor(filesMetadata, fileObjects) {
constructor(filesMetadata, fileObjects, options) {
this.filesMetadata = Array.from(filesMetadata)
this.fileObjects = fileObjects
this.timestamp = new Date()
this.options = options
}
get uploadMetadata() {
@ -38,12 +100,14 @@ class Upload {
return {
files: this.filesMetadata,
timestamp: this.timestamp,
...this.options
}
}
async create() {
let req = new Request(endPoints.upload , {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify(this.uploadMetadata)
})
@ -52,44 +116,45 @@ class Upload {
.catch((e) => { console.error(e) })
if (!res.ok) {
throw(`${res.status}: ` + (await res.json()).error)
}
return res.json().then(data => {
let { result: { id: id } } = data
let { upload_id: id } = data
this.uploadId = id
return data
})
}
async checkstatus(){
return pollUploadStatus(this.uploadId)
}
async send(){
console.log(`Sending with id ${this.uploadId}`)
let formData = new FormData();
for (let file of this.fileObjects) {
console.log(file)
formData.append('upload[]', file)
}
let req = new Request(endPoints.upload + this.uploadId, {
let req = new Request(endPoints.upload + '/' + this.uploadId, {
method: 'PUT',
credentials: 'same-origin',
body: formData
})
let res = await fetch(req)
.catch((e) => {console.error(e)})
if (!res.ok){
throw(`${res.status}: ` + (await res.json()).error)
}
return res.json()
}
}
export default {
endpoint: apiEndpoint,
endPoints: endPoints,
Upload: Upload
Upload: Upload,
pollUploadStatus: pollUploadStatus,
checkUploadStatus: checkUploadStatus,
download: download,
pollInvoice: pollInvoice,
adminInfo: adminInfo
}

@ -1,17 +1,65 @@
import './styles/index.css';
import 'tachyons'
import './styles/index.css'
import Vue from 'vue';
import App from './App.vue';
import Vue from 'vue'
import store from './store.js'
import Home from './Home.vue'
import App from './App.vue'
import UploadView from './UploadView.vue'
import DownloadView from './DownloadView.vue'
import AdminView from './AdminView.vue'
import GetWorker from './workerInterface.js'
import Router from 'vue-router'
import Api from './api.js'
//window.api = Api
Vue.use(Router)
const router = new Router({
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path:'/u/:uploadId',
name: 'upload',
component: UploadView,
props: true,
},
{
path:'/d/:dlId',
name: 'download',
component: DownloadView,
props: true,
},
{
// Redeem view
path:'/r/:adminToken',
name: 'admin',
component: AdminView,
props: true,
}
]
})
//const router = new VueRouter({
//routes
//})
window.app = new Vue({
el: '#app',
const app = new Vue({
template: '<App/>',
components: { App }
});
components: { App },
router: router,
store
}).$mount('#app')
window.worker = GetWorker('main');
//window.worker = GetWorker('main')

@ -0,0 +1,79 @@
import Vue from 'vue'
import Vuex from 'vuex'
import { mapState } from 'vuex'
Vue.use(Vuex)
const upload ={
state: {
status: {},
options: {
request_payment: true,
request_payment_amount: 1,
payment_currency: 'SAT'
},
},
mutations:{
setPayAmount (state, val){
state.options.request_payment_amount = val
},
setUpStatus (state, status) {
state.status = status
},
setCurrency(state, val){
state.options.payment_currency = val
},
setRequestPay(state, val){
state.options.request_payment = val
}
}
}
const down = {
state: {
files: [],
},
mutations: {
setFiles(state, val){
state.files = val
}
}
}
const base = {
state: {
invoice: {
settled: false,
},
uploadId: 0
},
mutations: {
setUploadId (state, id) {
state.uploadId = id
},
setInvoice (state, invoice) {
state.invoice = invoice
}
},
getters: {
paid(state) {
return state.invoice.settled
},
expired (state){
return ((new Date(state.invoice.expires_at*1000) - new Date() <= 0) &&
!state.paid)
},
unpaid (state){
return !state.invoice.settled
}
}
}
export default new Vuex.Store({
modules: {
upload,
down,
base
},
strict: true
})

@ -1,6 +1,6 @@
@import './variables.css';
body {
font-size: var(--test);
overflow: hidden;
}

@ -1,3 +1,4 @@
:root{
--test: 16px;
--qrcode-width: 350px;
}

@ -1,59 +0,0 @@
<template>
<div id="upload">
<input v-on:change="fileChange($event)" type="file" multiple />
<p>selected {{fileCount}} files</p>
<ul>
<li v-bind:files="files" v-for="file in files">
{{ file.name }}
<ul>
<li>size: {{ file.size / 1000 }} kB </li>
<li>type: {{ file.type }} </li>
</ul>
</li>
</ul>
</div>
</template>
<script charset="utf-8">
import GetWorker from './workerInterface.js';
const w = GetWorker('main');
w.listenTo('upload-id', (e) => {
console.log('vue received upload id ', e.data.id)
})
export default {
data(){
return {
fileCount: 0,
files: []
}
},
methods: {
fileChange(event) {
// reset
this.files = []
this.fileCount = event.target.files.length
this.files = event.target.files
w.post({
msg: 'new-upload',
payload: [...this.files],
})
// Get sha256 of files
// for (let file of this.files) {
// w.post({
// msg: 'file-sha256',
// payload: file
// })
// }
}
}
}
</script>

@ -5,6 +5,14 @@ import Api from './api.js'
const name = 'main'
// TODO
//if (window.isSecureContext) {
//// Page is a secure context so service workers are now available
//navigator.serviceWorker.register("/offline-worker.js").then(function () {
//...
//});
//}
function hexString(buffer) {
const byteArray = new Uint8Array(buffer);
@ -29,7 +37,7 @@ async function getSHA256(file){
}
async function newUpload(files){
async function newUpload(files, options){
let filesMetadata = await Promise.all(files.map(async (f) => {
return {
lastModified: f.lastModified,
@ -48,19 +56,37 @@ async function newUpload(files){
//}
let upload = new Api.Upload(filesMetadata, files)
let upload = new Api.Upload(filesMetadata, files, options)
// Ask permission to send a new upload
await upload.create()
.then(data => {
// Get the upload id
let { result: { id: id } } = data
//let { result: {
//upload_id: id,
//invoice: invoice ,
//status: status
//} } = data;
// Notify the UI
postMessage({msg: 'upload-id', id: id})
console.log('notifying')
console.log(Object.assign({msg: 'upload-invoice'}, data) )
postMessage( Object.assign({msg: 'upload-invoice'}, data) )
return data
})
.catch((e)=>{console.error('could not start upload ', e)})
// Send the files
upload.send()
// send the files
await upload.send()
.then(resp =>{
console.log('storage done')
console.log(resp);
})
.catch(err =>{
console.error(err)
})
}
@ -76,11 +102,26 @@ self.onmessage = e => {
break;
case 'new-upload':
newUpload(e.data.payload)
let { files, options } = e.data.payload;
newUpload(files, options)
break;
case 'watch-payment':
console.log('worker watching payment ' + e.data.uploadId);
Api.pollUploadStatus(e.data.uploadId)
.then((data)=>{
self.postMessage(Object.assign({msg: 'payment-received'}, data))
console.log('payment status update !')
console.log(data)
})
break;
default:
console.log(`${name} worker: need {msg: "message type", ... }`)
break;
}

@ -10,6 +10,9 @@ class Worker {
this.post({
msg: 'init'
})
}
}

@ -1707,6 +1707,18 @@ callsites@^3.0.0:
resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.0.0.tgz#fb7eb569b72ad7a45812f93fd9430a3e410b3dd3"
integrity sha512-tWnkwu9YEq2uzlBDI4RcLn8jrFvF9AOi8PxDNU3hZZjJcjkcRAq3vCI+vZcg1SuxISDYe86k9VZFwAxDiJGoAw==
camelcase@^5.0.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45"
integrity sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==
can-promise@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/can-promise/-/can-promise-0.0.1.tgz#7a7597ad801fb14c8b22341dfec314b6bd6ad8d3"
integrity sha512-gzVrHyyrvgt0YpDm7pn04MQt8gjh0ZAhN4ZDyCRtGl6YnuuK6b4aiUTD7G52r9l4YNmxfTtEscb92vxtAlL6XQ==
dependencies:
window-or-global "^1.0.1"
caniuse-api@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"
@ -1811,6 +1823,15 @@ cli-width@^2.0.0:
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=
cliui@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49"
integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==
dependencies:
string-width "^2.1.1"
strip-ansi "^4.0.0"
wrap-ansi "^2.0.0"
clone@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
@ -2019,7 +2040,7 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
cross-spawn@^6.0.4, cross-spawn@^6.0.5:
cross-spawn@^6.0.0, cross-spawn@^6.0.4, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@ -2272,6 +2293,11 @@ debug@^4.0.1, debug@^4.1.0, debug@^4.1.1:
dependencies:
ms "^2.1.1"
decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
decode-uri-component@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
@ -2372,6 +2398,11 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
dijkstrajs@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.1.tgz#d3cd81221e3ea40742cfcde556d4e99e98ddc71b"
integrity sha1-082BIh4+pAdCz83lVtTpnpjdxxs=
doctrine@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961"
@ -2494,6 +2525,13 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
end-of-stream@^1.1.0:
version "1.4.1"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43"
integrity sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==
dependencies:
once "^1.4.0"
entities@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
@ -2760,6 +2798,19 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
execa@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8"
integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==
dependencies:
cross-spawn "^6.0.0"
get-stream "^4.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
expand-brackets@^2.1.4:
version "2.1.4"
resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622"
@ -2897,6 +2948,13 @@ fill-range@^4.0.0:
repeat-string "^1.6.1"
to-regex-range "^2.1.0"
find-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==
dependencies:
locate-path "^3.0.0"
flat-cache@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0"
@ -2991,11 +3049,23 @@ gauge@~2.7.3:
strip-ansi "^3.0.1"
wide-align "^1.1.0"
get-caller-file@^1.0.1:
version "1.0.3"
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a"
integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==
get-port@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
integrity sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=
get-stream@^4.0.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5"
integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==
dependencies:
pump "^3.0.0"
get-value@^2.0.3, get-value@^2.0.6:
version "2.0.6"
resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28"
@ -3368,6 +3438,11 @@ invariant@^2.2.2:
dependencies:
loose-envify "^1.0.0"
invert-kv@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02"
integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==
is-absolute-url@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@ -3561,6 +3636,11 @@ is-resolvable@^1.0.0:
resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88"
integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==
is-stream@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
is-svg@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-3.0.0.tgz#9321dbd29c212e5ca99c4fa9794c714bcafa2f75"
@ -3605,6 +3685,11 @@ isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
isarray@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.4.tgz#38e7bcbb0f3ba1b7933c86ba1894ddfc3781bbb7"
integrity sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA==
isexe@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
@ -3800,6 +3885,13 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
lcid@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf"
integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==
dependencies:
invert-kv "^2.0.0"
left-pad@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e"
@ -3813,6 +3905,14 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
locate-path@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e"
integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==
dependencies:
p-locate "^3.0.0"
path-exists "^3.0.0"
lodash.clone@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6"
@ -3872,6 +3972,13 @@ magic-string@^0.22.4:
dependencies:
vlq "^0.2.2"
map-age-cleaner@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a"
integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==
dependencies:
p-defer "^1.0.0"
map-cache@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@ -3898,6 +4005,15 @@ mdn-data@~1.1.0:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-1.1.4.tgz#50b5d4ffc4575276573c4eedb8780812a8419f01"
integrity sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==
mem@^4.0.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/mem/-/mem-4.2.0.tgz#5ee057680ed9cb8dad8a78d820f9a8897a102025"
integrity sha512-5fJxa68urlY0Ir8ijatKa3eRz5lwXnRCTvo9+TbTGAuTFJOwpGcY0X05moBd0nW45965Njt4CDI2GFQoG8DvqA==
dependencies:
map-age-cleaner "^0.1.1"
mimic-fn "^2.0.0"
p-is-promise "^2.0.0"
merge-source-map@1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.0.4.tgz#a5de46538dae84d4114cc5ea02b4772a6346701f"
@ -3966,6 +4082,11 @@ mimic-fn@^1.0.0:
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==
mimic-fn@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.0.0.tgz#0913ff0b121db44ef5848242c38bbb35d44cabde"
integrity sha512-jbex9Yd/3lmICXwYT6gA/j2mNQGU48wCh/VzRd+/Y/PjYQtlg1gLMdZqvu9s/xH7qKvngxRObl56XZR609IMbA==
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
@ -4184,6 +4305,13 @@ npm-packlist@^1.1.6:
ignore-walk "^3.0.1"
npm-bundled "^1.0.1"
npm-run-path@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=
dependencies:
path-key "^2.0.0"
npmlog@^4.0.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
@ -4279,7 +4407,7 @@ on-finished@~2.3.0:
dependencies:
ee-first "1.1.1"
once@^1.3.0:
once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
@ -4334,6 +4462,15 @@ os-homedir@^1.0.0:
resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3"
integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M=
os-locale@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a"
integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==
dependencies:
execa "^1.0.0"
lcid "^2.0.0"
mem "^4.0.0"
os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
@ -4347,6 +4484,40 @@ osenv@^0.1.4:
os-homedir "^1.0.0"
os-tmpdir "^1.0.0"
p-defer@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c"
integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=
p-finally@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae"
integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=
p-is-promise@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5"
integrity sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==
p-limit@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2"
integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==
dependencies:
p-try "^2.0.0"
p-locate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4"
integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==
dependencies:
p-limit "^2.0.0"
p-try@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.1.0.tgz#c1a0f1030e97de018bb2c718929d2af59463e505"
integrity sha512-H2RyIJ7+A3rjkwKC2l5GGtU4H1vkxKCAGsWasNVd0Set+6i4znxbWy6/j16YDPJDWxhsgZiKAstMEP8wCdSpjA==
pako@^0.2.5:
version "0.2.9"
resolved "https://registry.yarnpkg.com/pako/-/pako-0.2.9.tgz#f3f7522f4ef782348da8161bad9ecfd51bf83a75"
@ -4483,6 +4654,11 @@ path-dirname@^1.0.0:
resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0"
integrity sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=
path-exists@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515"
integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=
path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
@ -4493,7 +4669,7 @@ path-is-inside@^1.0.2:
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=
path-key@^2.0.1:
path-key@^2.0.0, path-key@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40"
integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=
@ -4529,6 +4705,11 @@ pn@^1.1.0:
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
pngjs@^3.3.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-3.4.0.tgz#99ca7d725965fb655814eaf65f38f12bbdbf555f"
integrity sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==
posix-character-classes@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@ -4951,6 +5132,14 @@ public-encrypt@^4.0.0:
randombytes "^2.0.1"
safe-buffer "^5.1.2"
pump@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64"
integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==
dependencies:
end-of-stream "^1.1.0"
once "^1.3.1"
punycode@1.3.2:
version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
@ -4971,6 +5160,17 @@ q@^1.1.2:
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=
qrcode@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.3.3.tgz#5ef50c0c890cffa1897f452070f0f094936993de"
integrity sha512-SH7V13AcJusH3GT8bMNOGz4w0L+LjcpNOU/NiOgtBhT/5DoWeZE6D5ntMJnJ84AMkoaM4kjJJoHoh9g++8lWFg==
dependencies:
can-promise "0.0.1"
dijkstrajs "^1.0.1"
isarray "^2.0.1"
pngjs "^3.3.0"
yargs "^12.0.5"
qs@~6.5.2:
version "6.5.2"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
@ -5235,6 +5435,16 @@ request@^2.72.0, request@^2.87.0:
tunnel-agent "^0.6.0"
uuid "^3.3.2"
require-directory@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I=
require-main-filename@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1"
integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=
resolve-from@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748"
@ -5380,7 +5590,7 @@ serve-static@^1.12.4:
parseurl "~1.3.2"
send "0.16.2"
set-blocking@~2.0.0:
set-blocking@^2.0.0, set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
@ -5652,7 +5862,7 @@ string-width@^1.0.1:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
"string-width@^1.0.2 || 2", string-width@^2.1.0:
"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
@ -5704,6 +5914,11 @@ strip-ansi@^5.0.0, strip-ansi@^5.1.0:
dependencies:
ansi-regex "^4.1.0"
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=
strip-json-comments@^2.0.1, strip-json-comments@~2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
@ -5779,6 +5994,11 @@ table@^5.2.3:
slice-ansi "^2.1.0"
string-width "^3.0.0"
tachyons@4.10.0:
version "4.10.0"
resolved "https://registry.yarnpkg.com/tachyons/-/tachyons-4.10.0.tgz#87fa088e88d9759983f2a1b0efd01d767bbef87b"
integrity sha512-50KOizE6V9QB7LdQqVLsLw17dnBzxonC2JWhmbu8xnQYUoyqV+Gcx678NwVM+O8A7jljobtC6QMl/hc8UZOzAA==
tar@^4:
version "4.4.8"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
@ -6139,6 +6359,11 @@ vue-hot-reload-api@^2.3.3:
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf"
integrity sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==
vue-router@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.2.tgz#dedc67afe6c4e2bc25682c8b1c2a8c0d7c7e56be"
integrity sha512-opKtsxjp9eOcFWdp6xLQPLmRGgfM932Tl56U9chYTnoWqKxQ8M20N7AkdEbM5beUh6wICoFGYugAX9vQjyJLFg==
vue-template-compiler@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.9.tgz#26600415ff81a7a241aebc2d4e0abaa0f1a07915"
@ -6157,6 +6382,11 @@ vue@^2.6.8:
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.9.tgz#415c1cc1a5ed00c8f0acdd0a948139d12b7ea6b3"
integrity sha512-t1+tvH8hybPM86oNne3ZozCD02zj/VoZIiojOBPJLjwBn7hxYU5e1gBObFpq8ts1NEn1VhPf/hVXBDAJ3X5ljg==
vuex@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.1.0.tgz#634b81515cf0cfe976bd1ffe9601755e51f843b9"
integrity sha512-mdHeHT/7u4BncpUZMlxNaIdcN/HIt1GsGG5LKByArvYG/v6DvHcOxvDCts+7SRdCoIRGllK8IMZvQtQXLppDYg==
w3c-hr-time@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
@ -6206,6 +6436,11 @@ whatwg-url@^7.0.0:
tr46 "^1.0.1"
webidl-conversions "^4.0.2"
which-module@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which@^1.2.9:
version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
@ -6220,11 +6455,24 @@ wide-align@^1.1.0:
dependencies:
string-width "^1.0.2 || 2"
window-or-global@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/window-or-global/-/window-or-global-1.0.1.tgz#dbe45ba2a291aabc56d62cf66c45b7fa322946de"
integrity sha1-2+RboqKRqrxW1iz2bEW3+jIpRt4=
wordwrap@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=
dependencies:
string-width "^1.0.1"
strip-ansi "^3.0.1"
wrappy@1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
@ -6254,6 +6502,11 @@ xtend@^4.0.0, xtend@~4.0.1:
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
"y18n@^3.2.1 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
yallist@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
@ -6263,3 +6516,29 @@ yallist@^3.0.0, yallist@^3.0.2:
version "3.0.3"
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9"
integrity sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==
yargs-parser@^11.1.1:
version "11.1.1"
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4"
integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
yargs@^12.0.5:
version "12.0.5"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13"
integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==
dependencies:
cliui "^4.0.0"
decamelize "^1.2.0"
find-up "^3.0.0"
get-caller-file "^1.0.1"
os-locale "^3.0.0"
require-directory "^2.1.1"
require-main-filename "^1.0.1"
set-blocking "^2.0.0"
string-width "^2.0.0"
which-module "^2.0.0"
y18n "^3.2.1 || ^4.0.0"
yargs-parser "^11.1.1"

@ -0,0 +1,15 @@
package ws
// Websocket message types
const (
invoicePaid = iota
)
var messageTypes = map[int]string{
invoicePaid: "invoice-paid",
}
type ProtoMessage struct {
Type string `json:"type"`
Data interface{} `json:"data"`
}

@ -0,0 +1,274 @@
package ws
import (
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
"git.sp4ke.com/sp4ke/bit4sat/bus"
"git.sp4ke.com/sp4ke/bit4sat/db"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/mediocregopher/radix/v3"
"github.com/segmentio/ksuid"
)
const (
// Time allowed to write message to peer
writeWait = 10 * time.Second
// Time allowed to read the next pong message from the client.
pongWait = 30 * time.Second
// Send pings to client with this period. Must be less than pongWait.
pingPeriod = (pongWait * 9) / 10
//pingPeriod = 5 * time.Second
// Maximum message size
maxMessageSize = 512
WebsocketIdName = "websocket-id"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// Keep reference to clients so they can be used in goroutines
var C = make(map[*Client]bool)
// Interface to talk to websocket
type Client struct {
id ksuid.KSUID
// websocket connection
conn *websocket.Conn
// PubSub message channel for this websocket
subChannel chan radix.PubSubMessage
// Name of main subscribed channel for this websocket
channelName string
// upload ids registered to this client
uploadId string
// upload notifications channel
uploadNotifChannel chan radix.PubSubMessage
}
func (c *Client) readPump() {
defer func() {
// Delete client from referenc list
delete(C, c)
// Close ws connection
c.conn.Close()
}()
c.conn.SetReadLimit(maxMessageSize)
c.conn.SetReadDeadline(time.Now().Add(pongWait))
c.conn.SetPongHandler(func(string) error {
//log.Println("pong")
c.conn.SetReadDeadline(time.Now().Add(pongWait))
return nil
})
for {
_, message, err := c.conn.ReadMessage()
if err != nil {
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway,
websocket.CloseAbnormalClosure) {
log.Printf("error: %v", err)
}
break
}
recvData := make(map[string]interface{})
json.Unmarshal(message, &recvData)
log.Println(recvData)
}
}
func (c *Client) writePump() {
pingTicker := time.NewTicker(pingPeriod)
defer func() {
log.Println("websocket closing")
pingTicker.Stop()
c.conn.Close()
}()
// Subscribe to general notifications for this client
log.Printf("socket subscribing to %s", c.channelName)
if err := db.DB.RedisPubSub.Subscribe(c.subChannel,
c.channelName); err != nil {
log.Println(err)
return
}
// subscribe to notifications related to this channel's registered upload
// ids
if err := db.DB.RedisPubSub.PSubscribe(c.uploadNotifChannel,
fmt.Sprintf("%s_*", bus.UploadUpdateChannelPrefix)); err != nil {
log.Println(err)
return
}
for {
select {
case msg := <-c.subChannel:
//log.Printf("received msg %s on socket main channel", msg)
jsonMsg := bus.Message{}
if err := json.Unmarshal(msg.Message, &jsonMsg); err != nil {
log.Printf("ws error reading from pubsub: %s", err)
break
}
if jsonMsg.Type == bus.SetUploadId {
log.Printf("registering uploadId: %s to socket", jsonMsg.UploadId)
c.uploadId = jsonMsg.UploadId
}
case msg := <-c.uploadNotifChannel:
log.Printf("websocket received upload paid notification on channel %s", msg.Channel)
log.Printf("our registered ids are %s", c.uploadId)
// If we have no upload id registered for this client break
if c.uploadId == "" {
continue
}
// Check if the message matches our upload id
slice := strings.SplitN(msg.Channel, "_", 3)
if len(slice) != 3 {
log.Printf("error decoding channel name %s", msg.Channel)
}
msgTarget := slice[2]
// If the target is any of our registered upload ids
// broadcast the message
if c.uploadId == msgTarget {
log.Println("this message is for us sending to client")
jsonMsg := bus.Message{}
err := json.Unmarshal(msg.Message, &jsonMsg)
if err != nil {
log.Printf("unmarshal error %s", err)
break
}
switch jsonMsg.Type {
case bus.PaymentReceived:
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
w, err := c.conn.NextWriter(websocket.TextMessage)
if err != nil {
log.Println(err)
return
}
socketMsg := ProtoMessage{}
socketMsg.Type = messageTypes[invoicePaid]
socketMsg.Data = jsonMsg.Data
enc := json.NewEncoder(w)
err = enc.Encode(socketMsg)
if err != nil {
log.Println(err)
return
}
// No need to encode the message it's already in json
if err = w.Close(); err != nil {
fmt.Println(err)
return
}
}
// Handle different message types
}
case <-pingTicker.C:
//log.Println("ping")
c.conn.SetWriteDeadline(time.Now().Add(writeWait))
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
log.Printf("websocket ping error: %s", err)
return
}
}
}
}
func Serve(c *gin.Context) {
var err error
log.Println("websocket request")
session := sessions.Default(c)
// First check if cookie is already set with upload id in case this is a
// reconnection
//
var finalSessId ksuid.KSUID
socketSessionId := session.Get(WebsocketIdName)
if socketSessionId == nil {
// Create unique id for this session in order to store it
// in the websocket bus
finalSessId, err = ksuid.NewRandomWithTime(time.Now())
if err != nil {
log.Println("error generating ksuid for websocket")
return
}
log.Printf("using socket id: %s", finalSessId)
session.Set(WebsocketIdName, finalSessId.String())
session.Save()
log.Printf("Writing socket session id to header: %s", c.Writer.Header())
} else {
// Reuse socket session id
log.Printf("reusing websocket id %s", socketSessionId)
finalSessId, err = ksuid.Parse(socketSessionId.(string))
if err != nil {
log.Println("could not parse websocket session id")
}
}
conn, err := upgrader.Upgrade(c.Writer, c.Request, c.Writer.Header())
if err != nil {
if _, ok := err.(websocket.HandshakeError); !ok {
log.Printf("handshake error: %s", err)
}
log.Println(err)
return
}
client := &Client{
id: finalSessId,
conn: conn,
subChannel: make(chan radix.PubSubMessage),
uploadNotifChannel: make(chan radix.PubSubMessage),
channelName: fmt.Sprintf("%s_%s", bus.WebsocketPubSubPrefix, finalSessId),
}
C[client] = true
go client.writePump()
go client.readPump()
}
Loading…
Cancel
Save