Skip to content

Commit

Permalink
Squash repo
Browse files Browse the repository at this point in the history
  • Loading branch information
LightningTipBot committed Aug 22, 2021
0 parents commit d6f8380
Show file tree
Hide file tree
Showing 36 changed files with 2,647 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
config.yaml
config.yaml*
data/*
!data/.placeholder
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2020 neurolib-dev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<p align="center">
<img alt="logo" src="resources/logo_round.png" >
</p>


# @LightningTipBot 🏅

A tip bot for Telegram group chats that can send and receive Bitcoin via the Lightning Network ⚡️. It can also be used as a personal Lightning wallet by anyone who has a Telegram account.

This repository contains everything you need to set up and run your own tip bot. If you simply want to use this bot in your group chat without having to install anything just start a conversation with [@LightningTipBot](https://t.me/LightningTipBot) and invite it into your chat.

## Setting up the bot
### Installation
To build the bot from source, clone the repository and compile the source code.

```
git clone https://github.com/LightningTipBot/LightningTipBot.git
cd LightningTipBot
go build .
cp config.yaml-example config.yaml
```

Alternatively, you can download binary releases [here](https://github.com/LightningTipBot/LightningTipBot/releases) **(note: no realses yet)**.

After you have configured the bot, start it using the command

```
./LightningTipBot
```

### Configuration

You need to edit `config.yaml` before starting the bot.

#### Create a Telegram bot

First, create a new Telegram bot by starting a conversation with the [@BotFather](https://core.telegram.org/bots#6-botfather). After you have created your bot, you will get an **Api Token** which you need to add to `telegram_api_key` in config.yaml accordingly.

#### Set up LNbits

You can either use your own LNbits instance (recommended) or create an account at [lnbits.com](https://lnbits.com/) to use their custodial service (easy).

1. Create a wallet in LNbits (`lnbits_url`).
2. Get the **Admin key** in the API Info tab of your user (`lnbits_admin_key`).
3. Enable the User Manager extension.
4. Get the **Admin ID** of the User Manager (`lnbits_admin_id`).

#### Getting LNbits keys

<p align="center">
<img alt="How to set up a lnbits wallet and the User Manager extension." src="resources/lnbits_setup.png" >
</p>

#### More configuration
* `webhook_server`: URL that can reach the bot. This is used for creating webhooks with LNbits to receive notifications about payments (optional).
* `db_path`: User database file path.
* `transactions_path`: Transaction log file path.
* `message_dispose_duration`: Duration in seconds after which commands will be deleted from channel (only if the bot is channel admin).

## Features

### Commands
```
/tip 🏅 Reply to a message to tip it: /tip <amount> [<memo>]
/balance 👑 Check your balance: /balance
/send 💸 Send funds to a user: /send <amount> <@username> [<memo>]
/invoice ⚡️ Create a Lightning invoice to receive payments: /invoice <amount> [<memo>]
/pay ⚡️ Pay a Lightning invoice: /pay <invoice>
/help 📖 Read this help.
```

### Live tooltips

The bot replies to a tipped message to indicate to all participants how many and what amount of tips a post has received. This tooltip will be updated as new tips are given to a post.

<p align="center">
<img alt="How to set up a lnbits wallet and the User Manager extension." src="resources/tooltips.png" >
</p>

### Auto-delete commands

To minimize the clutter all the heavy tipping can cause in a group chat, the bot will remove all failed commands (for example due to a syntax error) from the chat immediately. All successful commands will stay visible for `message_dispose_duration` seconds (default 10s) and then be removed. The tips will sill be visible for everyone in the Live tooltip. This feature only works, if the bot is made admin of the group.

## Made with
* [LNbits](https://github.com/lnbits/lnbits) – Free and open-source lightning-network wallet/accounts system.
* [telebot](https://github.com/tucnak/telebot) – A Telegram bot framework in Go.
* [gozxing](https://github.com/makiuchi-d/gozxing) – barcode image processing library in Go.
* [ln-decodepay](https://github.com/fiatjaf/ln-decodepay) – Lightning Network BOLT11 invoice decoder.

38 changes: 38 additions & 0 deletions amounts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"errors"
"strconv"
"strings"
)

func getArgumentFromCommand(input string, which int) (output string, err error) {
if len(strings.Split(input, " ")) < which+1 {
return "", errors.New("Message doesn't contain enough arguments")
}
output = strings.Split(input, " ")[which]
return output, nil
}

func decodeAmountFromCommand(input string) (amount int, err error) {
if len(strings.Split(input, " ")) < 2 {
errmsg := "Message doesn't contain any amount"
// log.Errorln(errmsg)
return 0, errors.New(errmsg)
}
amount, err = getAmount(input)
return amount, nil
}

func getAmount(input string) (amount int, err error) {
amount, err = strconv.Atoi(strings.Split(input, " ")[1])
if err != nil {
return 0, err
}
if amount < 1 {
errmsg := "Error: Amount must be greater than 0"
// log.Errorln(errmsg)
return 0, errors.New(errmsg)
}
return amount, err
}
40 changes: 40 additions & 0 deletions balance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package main

import (
"fmt"

log "github.com/sirupsen/logrus"

tb "gopkg.in/tucnak/telebot.v2"
)

func (bot TipBot) balanceHandler(m *tb.Message) {
log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, m.Text)
// reply only in private message
if m.Chat.Type != tb.ChatPrivate {
// delete message
NewMessage(m).Dispose(0, bot.telegram)
}
// first check whether the user is initialized
fromUser, err := GetUser(m.Sender, bot)
if err != nil {
log.Errorf("[/balance] Error: %s", err)
return
}
if !fromUser.Initialized {
bot.startHandler(m)
return
}

usrStr := GetUserStr(m.Sender)
balance, err := bot.GetUserBalance(m.Sender)
if err != nil {
log.Errorf("[/balance] Error fetching %s's balance: %s", usrStr, err)
bot.telegram.Send(m.Sender, "🚫 Error fetching your balance. Please try again later.")
return
}

log.Infof("[/balance] %s's balance: %d sat\n", usrStr, balance)
bot.telegram.Send(m.Sender, fmt.Sprintf("👑 *Your balance:* %d sat", balance))
return
}
127 changes: 127 additions & 0 deletions bot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package main

import (
"fmt"
"strings"
"sync"
"time"

log "github.com/sirupsen/logrus"

"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
"gopkg.in/tucnak/telebot.v2"
tb "gopkg.in/tucnak/telebot.v2"

"gorm.io/gorm"
)

type TipBot struct {
database *gorm.DB
logger *gorm.DB
telegram *telebot.Bot
client *lnbits.Client
tips map[int][]*Message
}

var (
paymentConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true}
btnCancelPay = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_pay")
btnPay = paymentConfirmationMenu.Data("✅ Pay", "confirm_pay")
sendConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true}
btnCancelSend = sendConfirmationMenu.Data("🚫 Cancel", "cancel_send")
btnSend = sendConfirmationMenu.Data("✅ Send", "confirm_send")

botWalletInitialisation = sync.Once{}
telegramHandlerRegistration = sync.Once{}
)

// NewBot migrates data and creates a new bot
func NewBot() TipBot {
db, txLogger := migration()
return TipBot{
database: db,
logger: txLogger,
tips: make(map[int][]*Message, 0),
}
}

// newTelegramBot will create a new telegram bot.
func newTelegramBot() *tb.Bot {
tgb, err := tb.NewBot(tb.Settings{
Token: Configuration.ApiKey,
Poller: &tb.LongPoller{Timeout: 60 * time.Second},
ParseMode: tb.ModeMarkdown,
})
if err != nil {
panic(err)
}
return tgb
}

// initBotWallet will create / initialize the bot wallet
// todo -- may want to derive user wallets from this specific bot wallet (master wallet), since lnbits usermanager extension is able to do that.
func (bot TipBot) initBotWallet() error {
botWalletInitialisation.Do(func() {
err := bot.initWallet(bot.telegram.Me)
if err != nil {
log.Errorln(fmt.Sprintf("[initBotWallet] Could not initialize bot wallet: %s", err.Error()))
return
}
})
return nil
}

// registerTelegramHandlers will register all telegram handlers.
func (bot TipBot) registerTelegramHandlers() {
telegramHandlerRegistration.Do(func() {
// Set up handlers
var endpointHandler = map[string]interface{}{
"/tip": bot.tipHandler,
"/pay": bot.confirmPaymentHandler,
"/invoice": bot.invoiceHandler,
"/balance": bot.balanceHandler,
"/start": bot.startHandler,
"/send": bot.confirmSendHandler,
"/help": bot.helpHandler,
"/info": bot.infoHandler,
tb.OnPhoto: bot.privatePhotoHandler,
tb.OnText: bot.anyTextHandler,
}
// assign handler to endpoint
for endpoint, handler := range endpointHandler {
log.Debugf("Registering: %s", endpoint)
bot.telegram.Handle(endpoint, handler)

// if the endpoint is a string command (not photo etc)
if strings.HasPrefix(endpoint, "/") {
// register upper case versions as well
bot.telegram.Handle(strings.ToUpper(endpoint), handler)
}
}

// button handlers
// for /pay
bot.telegram.Handle(&btnPay, bot.payHandler)
bot.telegram.Handle(&btnCancelPay, bot.cancelPaymentHandler)
// for /send
bot.telegram.Handle(&btnSend, bot.sendHandler)
bot.telegram.Handle(&btnCancelSend, bot.cancelSendHandler)
})
}

// Start will initialize the telegram bot and lnbits.
func (bot TipBot) Start() {
// set up lnbits api
bot.client = lnbits.NewClient(Configuration.LnbitsKey, Configuration.LnbitsUrl)
// set up telebot
bot.telegram = newTelegramBot()
log.Infof("[Telegram] Authorized on account @%s", bot.telegram.Me.Username)
// initialize the bot wallet
err := bot.initBotWallet()
if err != nil {
log.Errorf("Could not initialize bot wallet: %s", err.Error())
}
bot.registerTelegramHandlers()
lnbits.NewWebhook(Configuration.WebhookServer, bot.telegram, bot.client, bot.database)
bot.telegram.Start()
}
13 changes: 13 additions & 0 deletions botfather-setcommands.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Instructions:

1 - Talk to the botfather
2 - Use: /setcommands
3 - Select your tip bot
4 - Paste this:

help - Read the help.
balance - Check your balance.
tip - Reply to a message to tip it.
send - Send funds to a user.
invoice - Create a Lightning invoice to receive payments.
pay - Pay a Lightning invoice.
21 changes: 21 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import "github.com/jinzhu/configor"

var Configuration = struct {
AdminKey string `json:"lnbits_admin_id" yaml:"lnbits_admin_id"`
LnbitsKey string `json:"lnbits_admin_key" yaml:"lnbits_admin_key"`
LnbitsUrl string `json:"lnbits_url" yaml:"lnbits_url"`
WebhookServer string `json:"webhook_server" yaml:"webhook_server"`
ApiKey string `json:"telegram_api_key" yaml:"telegram_api_key"`
DbPath string `json:"db_path" yaml:"db_path"`
TransactionsPath string `json:"transactions_path" yaml:"transactions_path"`
MessageDisposeDuration int64 `json:"message_dispose_duration" yaml:"message_dispose_duration"`
}{}

func init() {
err := configor.Load(&Configuration, "config.yaml")
if err != nil {
panic(err)
}
}
8 changes: 8 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
telegram_api_key: "1234"
webhook_server: "http://0.0.0.0:1234"
lnbits_url: "http://*.onion:1234"
lnbits_admin_key: "1234"
lnbits_admin_id: "3212"
db_path: "data/bot.db"
transactions_path: "data/transactions.db"
message_dispose_duration: 10
Empty file added data/.placeholder
Empty file.
Loading

0 comments on commit d6f8380

Please sign in to comment.