Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 88 additions & 0 deletions .github/workflows/release-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
name: Dev Release

on:
push:
branches:
- dev

permissions:
contents: write

jobs:
release-dev:
runs-on: ubuntu-latest
env:
BOT_TOKEN: ${{ secrets.MAIN_BOT_TOKEN }}
CHAT_ID: ${{ vars.DEV_RELEASE_CHAT_IDS }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.21'

- name: Get short SHA
id: vars
run: echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT

- name: Calculate next version
id: calc_version
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
dry_run: true
tag_prefix: "v"

- name: Tag dev build
id: tag_version
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
custom_tag: v${{ steps.calc_version.outputs.major }}.${{ steps.calc_version.outputs.minor }}.${{ steps.calc_version.outputs.patch }}-${{ steps.vars.outputs.sha_short }}
tag_prefix: ""

- name: Create local tag
run: git tag ${{ steps.tag_version.outputs.new_tag }}

- name: Run GoReleaser (dev)
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Mark as prerelease
run: gh release edit ${{ steps.tag_version.outputs.new_tag }} --prerelease
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Notify Telegram (Dev)
if: success()
run: |
IFS=',' read -ra TARGETS <<< "${{ env.CHAT_ID }}"
for target in "${TARGETS[@]}"; do
target=$(echo "$target" | xargs)
if [[ -z "$target" ]]; then
continue
fi
if [[ "$target" == *":"* ]]; then
chat_id=${target%%:*}
thread_id=${target#*:}
curl -s -X POST https://api.telegram.org/bot${{ env.BOT_TOKEN }}/sendMessage \
-d chat_id="$chat_id" \
-d message_thread_id="$thread_id" \
-d text="🧪 *New Dev Build Published!*%0A%0A*Tag:* ${{ steps.tag_version.outputs.new_tag }}%0A*Commit:* ${{ github.sha }}%0A*Author:* ${{ github.actor }}%0A%0A[View Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.tag_version.outputs.new_tag }})" \
-d parse_mode="Markdown"
else
curl -s -X POST https://api.telegram.org/bot${{ env.BOT_TOKEN }}/sendMessage \
-d chat_id="$target" \
-d text="🧪 *New Dev Build Published!*%0A%0A*Tag:* ${{ steps.tag_version.outputs.new_tag }}%0A*Commit:* ${{ github.sha }}%0A*Author:* ${{ github.actor }}%0A%0A[View Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.tag_version.outputs.new_tag }})" \
-d parse_mode="Markdown"
fi
done
71 changes: 71 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: Release

on:
pull_request:
types: [closed]
branches:
- main

permissions:
contents: write

jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
env:
BOT_TOKEN: ${{ secrets.MAIN_BOT_TOKEN }}
CHAT_ID: ${{ vars.RELEASE_CHAT_IDS }}
PR_TITLE: ${{ github.event.pull_request.title }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '1.21'

- name: Bump version and push tag
id: tag_version
uses: mathieudutour/github-tag-action@v6.1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
default_bump: patch

- name: Create local tag
run: git tag ${{ steps.tag_version.outputs.new_tag }}

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Notify Telegram Success
if: success()
run: |
IFS=',' read -ra TARGETS <<< "${{ env.CHAT_ID }}"
for target in "${TARGETS[@]}"; do
target=$(echo "$target" | xargs)
if [[ "$target" == *":"* ]]; then
chat_id=${target%%:*}
thread_id=${target#*:}
curl -s -X POST https://api.telegram.org/bot${{ env.BOT_TOKEN }}/sendMessage \
-d chat_id="$chat_id" \
-d message_thread_id="$thread_id" \
-d text="🚀 *New Release Published!*%0A%0A*Version:* ${{ steps.tag_version.outputs.new_tag }}%0A*Title:* ${PR_TITLE}%0A*Author:* ${{ github.actor }}%0A%0A[View Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.tag_version.outputs.new_tag }})" \
-d parse_mode="Markdown"
else
curl -s -X POST https://api.telegram.org/bot${{ env.BOT_TOKEN }}/sendMessage \
-d chat_id="$target" \
-d text="🚀 *New Release Published!*%0A%0A*Version:* ${{ steps.tag_version.outputs.new_tag }}%0A*Title:* ${PR_TITLE}%0A*Author:* ${{ github.actor }}%0A%0A[View Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.tag_version.outputs.new_tag }})" \
-d parse_mode="Markdown"
fi
done

24 changes: 24 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
project_name: BitcoinDeepaBot
builds:
- env:
- CGO_ENABLED=1
goos:
- linux
goarch:
- amd64
flags:
- -a
- -installsuffix
- cgo
ldflags:
- -s -w
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
4 changes: 3 additions & 1 deletion internal/api/lightning.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import (
"github.com/LightningTipBot/LightningTipBot/internal"
"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
"github.com/LightningTipBot/LightningTipBot/internal/telegram"
"github.com/LightningTipBot/LightningTipBot/internal/utils"
"github.com/gorilla/mux"
"github.com/r3labs/sse"
)

type Service struct {
Bot *telegram.TipBot
Bot *telegram.TipBot
MemoCache *utils.Cache
}

type ErrorResponse struct {
Expand Down
36 changes: 34 additions & 2 deletions internal/api/send.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,15 @@ func (s Service) Send(w http.ResponseWriter, r *http.Request) {
RespondError(w, "Authentication failed")
return
}

walletID := authenticatedWallet.(string)
wallet, exists := GetWhitelistedWallets()[walletID]
if !exists {
log.Errorf("[api/send] Authenticated wallet %s not found in configuration", walletID)
RespondError(w, "Invalid wallet configuration")
return
}

fromUsername := wallet.Username

// Validate request
Expand All @@ -150,6 +150,38 @@ func (s Service) Send(w http.ResponseWriter, r *http.Request) {
return
}

if req.Memo != "" {
memoLockKey := fmt.Sprintf("api_send_memo_%s", req.Memo)

// Try to acquire lock first to prevent concurrent processing
if success := s.MemoCache.SetNX(memoLockKey, "locked"); !success {
log.Warnf("[api/send] Transaction with memo '%s' is already processing", req.Memo)
RespondError(w, fmt.Sprintf("Transaction with memo '%s' is already processing", req.Memo))
return
}
// Unlock when done
defer s.MemoCache.Delete(memoLockKey)

// Check if transaction with this memo already exists in database
// We search for the memo in the transaction memo field
// The stored memo format is: "💸 API Send from @User to @User. Memo: <req.Memo>"
// So we search for the suffix "Memo: <req.Memo>"
var count int64
memoSearch := fmt.Sprintf("%%Memo: %s", req.Memo)
err := s.Bot.DB.Transactions.Model(&telegram.Transaction{}).Where("memo LIKE ? AND success = ?", memoSearch, true).Count(&count).Error
if err != nil {
log.Errorf("[api/send] Database error checking for duplicate memo: %v", err)
// Continue but log error - fail open or closed? Let's fail closed for safety
RespondError(w, "Internal server error checking transaction history")
return
}
if count > 0 {
log.Warnf("[api/send] Transaction with memo '%s' already completed", req.Memo)
RespondError(w, fmt.Sprintf("Transaction with memo '%s' already completed", req.Memo))
return
}
}

// Clean usernames (remove @ if present)
toIdentifier := strings.TrimPrefix(req.To, "@")

Expand Down
23 changes: 23 additions & 0 deletions internal/utils/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,26 @@ func (c *Cache) Get(key string) (string, bool) {
}
return item.value, true
}

func (c *Cache) Delete(key string) {
c.mutex.Lock()
defer c.mutex.Unlock()
delete(c.data, key)
}

// SetNX sets the key if it does not exist or has expired. Returns true if set, false if already exists.
func (c *Cache) SetNX(key string, value string) bool {
c.mutex.Lock()
defer c.mutex.Unlock()

item, exists := c.data[key]
if exists && time.Now().Before(item.expiration) {
return false
}

c.data[key] = CacheItem{
value: value,
expiration: time.Now().Add(c.ttl),
}
return true
}
9 changes: 7 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"runtime/debug"
"strings"
"time"

"github.com/LightningTipBot/LightningTipBot/internal"
"github.com/LightningTipBot/LightningTipBot/internal/api"
Expand All @@ -21,6 +22,7 @@ import (

"github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook"
"github.com/LightningTipBot/LightningTipBot/internal/price"
"github.com/LightningTipBot/LightningTipBot/internal/utils"
log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -89,7 +91,10 @@ func startApiServer(bot *telegram.TipBot) {
s.AppendAuthorizedRoute(`/lndhub/ext`, api.AuthTypeBearerBase64, api.AccessKeyTypeAdmin, bot.DB.Users, hub.Handle)

// starting api service
apiService := api.Service{Bot: bot}
apiService := api.Service{
Bot: bot,
MemoCache: utils.NewCache(time.Minute * 5),
}
s.AppendAuthorizedRoute(`/api/v1/paymentstatus/{payment_hash}`, api.AuthTypeBasic, api.AccessKeyTypeInvoice, bot.DB.Users, apiService.PaymentStatus, http.MethodPost)
s.AppendAuthorizedRoute(`/api/v1/invoicestatus/{payment_hash}`, api.AuthTypeBasic, api.AccessKeyTypeInvoice, bot.DB.Users, apiService.InvoiceStatus, http.MethodPost)
s.AppendAuthorizedRoute(`/api/v1/payinvoice`, api.AuthTypeBasic, api.AccessKeyTypeAdmin, bot.DB.Users, apiService.PayInvoice, http.MethodPost)
Expand All @@ -101,7 +106,7 @@ func startApiServer(bot *telegram.TipBot) {
if internal.IsAPISendEnabled() {
s.AppendRoute(`/api/v1/send`, api.WalletHMACMiddleware(apiService.Send), http.MethodPost)
log.Infof("API Send endpoint registered at /api/v1/send with wallet-based HMAC security")

// User balance endpoint with wallet-based HMAC security
s.AppendRoute(`/api/v1/userbalance`, api.WalletHMACMiddleware(apiService.UserBalance), http.MethodPost)
log.Infof("API UserBalance endpoint registered at /api/v1/userbalance with wallet-based HMAC security")
Expand Down
Loading