Compare commits
42 Commits
Author | SHA1 | Date |
---|---|---|
Chakib Benziane | 23f2e99f4a | 5 years ago |
Chakib Benziane | bd9a980133 | 5 years ago |
Chakib Benziane | 9ecdef717a | 5 years ago |
Chakib Benziane | aa90856f59 | 5 years ago |
Chakib Benziane | 44de8bc448 | 5 years ago |
Chakib Benziane | 4985b3aea3 | 5 years ago |
Chakib Benziane | fdd586d11c | 5 years ago |
Chakib Benziane | d8c8c6a49b | 5 years ago |
Chakib Benziane | 4105c33e25 | 5 years ago |
Chakib Benziane | 53f37ff619 | 5 years ago |
Chakib Benziane | 24fec00864 | 5 years ago |
Chakib Benziane | 4d764d057c | 5 years ago |
sp4ke | cefd7ba558 | 5 years ago |
sp4ke | 8f845374dd | 5 years ago |
Chakib Benziane | 5ff79cbec3 | 5 years ago |
Chakib Benziane | e97dc51f8d | 5 years ago |
Chakib Benziane | 0f5075acab | 5 years ago |
Chakib Benziane | a2ae0ce517 | 5 years ago |
Chakib Benziane | 6b74f86164 | 5 years ago |
Chakib Benziane | 314e3b7624 | 5 years ago |
Chakib Benziane | fd7cec6600 | 5 years ago |
Chakib Benziane | 23abc0b369 | 5 years ago |
Chakib Benziane | ea20251d07 | 5 years ago |
Chakib Benziane | 0d4a3d00e6 | 5 years ago |
Chakib Benziane | be1f3ae1b7 | 5 years ago |
Chakib Benziane | 849d5ebff5 | 5 years ago |
Chakib Benziane | 93a0260ecc | 5 years ago |
Chakib Benziane | 89fc1b2894 | 5 years ago |
Chakib Benziane | e4ca0e2701 | 5 years ago |
Chakib Benziane | c1900d52a3 | 5 years ago |
Chakib Benziane | bcd3160cfd | 5 years ago |
Chakib Benziane | b1e4ee4196 | 5 years ago |
Chakib Benziane | f22a691152 | 5 years ago |
Chakib Benziane | 38e55de611 | 5 years ago |
Chakib Benziane | 10949a9ba3 | 5 years ago |
Chakib Benziane | ae30315d16 | 5 years ago |
Chakib Benziane | bb90656197 | 5 years ago |
Chakib Benziane | f3299ad19c | 5 years ago |
Chakib Benziane | 634e94909c | 5 years ago |
Chakib Benziane | 6627ffcee7 | 5 years ago |
Chakib Benziane | f1a01ebe47 | 5 years ago |
Chakib Benziane | b31df12a3d | 5 years ago |
@ -0,0 +1,5 @@
|
||||
node_modules
|
||||
**/node_modules
|
||||
dist
|
||||
bit4sat
|
||||
file-storage
|
@ -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"]
|
@ -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 +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()
|
||||
}
|
@ -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"
|
||||
|
@ -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()
|
||||
//}
|
||||
}
|
@ -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)),
|
||||
})
|
||||
|
||||
}
|
@ -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
|
||||
}
|
@ -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.
@ -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 {{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,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>
|
@ -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…
Reference in New Issue