Add Mastodon, authenticated Twitter support.

This commit is contained in:
Syfaro 2018-10-06 15:57:51 -05:00
parent 44ba18063c
commit 2f4f6d8009
18 changed files with 266 additions and 16 deletions

View File

@ -4,3 +4,4 @@ README.md
telegram-furryimgbot*
.env
*.json
*.db*

1
.gitignore vendored
View File

@ -1,3 +1,4 @@
telegram-furryimgbot*
.env
*.json
*.db*

View File

@ -5,6 +5,9 @@ RUN apk add git g++
RUN go build -o main
FROM alpine
RUN apk add ca-certificates
WORKDIR /app
RUN mkdir /app/db
COPY --from=builder /app/main .
VOLUME /app/db
CMD ["./main"]

View File

@ -6,10 +6,16 @@ After being configured (optional, will only do SFW images from FurAffinity thoug
## Configuration
Items either need to be set in config.json as lowercase or environment variables.
* `TOKEN` - Telegram Bot API token from Botfather
* `DEBUG` - If debugging information should be printed
* `FA_A` - FurAffinity cookie 'a'
* `FA_B` - FurAffinity cookie 'b'
* `TWITTER_CONSUMER_KEY` - Twitter application consumer access key
* `TWITTER_SECRET_KEY` - Twitter application consumer secret key
If using Twitter credentials from users, the `db` folder must be persistent.
## Supported Sites

2
bot.go
View File

@ -23,7 +23,7 @@ func main() {
f.API.Debug = false
}
if f.Config.Get("debug") == "true" {
if f.API.Debug {
logger.Log.SetLevel(logrus.DebugLevel)
} else {
logger.Log.SetLevel(logrus.InfoLevel)

View File

@ -0,0 +1,144 @@
package command
import (
"encoding/json"
"strconv"
"strings"
"sync"
"github.com/Syfaro/finch"
"github.com/go-telegram-bot-api/telegram-bot-api"
"huefox.com/syfaro/telegram-furryimgbot/data"
"github.com/boltdb/bolt"
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
twitterOAuth1 "github.com/dghubble/oauth1/twitter"
)
type twitterAuthCommand struct {
finch.CommandBase
oauthConfig *oauth1.Config
credentials map[int][]string
credentialsSync *sync.Mutex
}
func (cmd *twitterAuthCommand) Init(c *finch.CommandState, f *finch.Finch) error {
cmd.CommandState = c
cmd.Finch = f
config := &oauth1.Config{
ConsumerKey: f.Config.Get("twitter_consumer_key").(string),
ConsumerSecret: f.Config.Get("twitter_secret_key").(string),
CallbackURL: "oob",
Endpoint: twitterOAuth1.AuthorizeEndpoint,
}
cmd.oauthConfig = config
cmd.credentials = make(map[int][]string)
cmd.credentialsSync = &sync.Mutex{}
return nil
}
func (twitterAuthCommand) Help() finch.Help {
return finch.Help{
Name: "Twitter Auth",
Description: "Authorize with your Twitter account to get locked posts",
Botfather: [][]string{
[]string{"twitter", "Authorize your Twitter account"},
},
}
}
func (twitterAuthCommand) ShouldExecute(message tgbotapi.Message) bool {
return finch.SimpleCommand("twitter", message.Text)
}
func (cmd *twitterAuthCommand) Execute(message tgbotapi.Message) error {
if !message.Chat.IsPrivate() {
return cmd.QuickReply(message, "You may only use this command in a private chat")
}
token, secret, err := cmd.oauthConfig.RequestToken()
if err != nil {
return err
}
cmd.credentialsSync.Lock()
cmd.credentials[message.From.ID] = []string{token, secret}
cmd.credentialsSync.Unlock()
cmd.SetWaiting(message.From.ID)
authURL, err := cmd.oauthConfig.AuthorizationURL(token)
if err != nil {
return err
}
b := &strings.Builder{}
b.WriteString("Please visit the following URL to authorize your Twitter account, then tell me the returned code.\n\n")
b.WriteString(authURL.String())
msg := tgbotapi.NewMessage(message.Chat.ID, b.String())
msg.ReplyMarkup = tgbotapi.ForceReply{ForceReply: true, Selective: true}
cmd.SendMessage(msg)
return nil
}
func (cmd *twitterAuthCommand) ExecuteWaiting(message tgbotapi.Message) error {
cmd.ReleaseWaiting(message.From.ID)
code := strings.Trim(message.Text, " ")
cmd.credentialsSync.Lock()
creds := cmd.credentials[message.From.ID]
cmd.credentialsSync.Unlock()
access, secret, err := cmd.oauthConfig.AccessToken(creds[0], creds[1], code)
if err != nil {
return err
}
err = data.DB.Update(func(tx *bolt.Tx) error {
b, err := tx.CreateBucketIfNotExists([]byte("twittercreds"))
if err != nil {
return err
}
bytes, err := json.Marshal(data.UserCred{
AccessToken: access,
SecretToken: secret,
})
if err != nil {
return err
}
return b.Put([]byte(strconv.Itoa(message.From.ID)), bytes)
})
if err != nil {
return err
}
token := oauth1.NewToken(access, secret)
client := twitter.NewClient(cmd.oauthConfig.Client(oauth1.NoContext, token))
user, _, err := client.Accounts.VerifyCredentials(nil)
if err != nil {
return err
}
return cmd.QuickReply(message, "Thank you, @"+user.ScreenName)
}
func init() {
finch.RegisterCommand(&twitterAuthCommand{})
}

View File

@ -25,7 +25,7 @@ func (i inline) Execute(f *finch.Finch, query tgbotapi.InlineQuery) error {
for _, site := range sites.KnownSites {
queryStr := strings.Trim(query.Query, " ")
if site.IsSupportedURL(queryStr) {
posts, e := site.GetImageURLs(queryStr)
posts, e := site.GetImageURLs(queryStr, *query.From)
err = e
if posts != nil && len(posts) != 0 {
for _, post := range posts {

View File

@ -9,7 +9,7 @@ import (
"huefox.com/syfaro/telegram-furryimgbot/sites"
)
var urlFinder = xurls.Strict
var urlFinder = xurls.Strict()
func init() {
finch.RegisterCommand(&messageCommand{})
@ -42,7 +42,7 @@ func (messageCommand) Help() finch.Help {
}
func (cmd messageCommand) processURL(site sites.Site, message tgbotapi.Message, url string) error {
posts, err := site.GetImageURLs(url)
posts, err := site.GetImageURLs(url, *message.From)
if err != nil {
return err
}

21
data/db.go Normal file
View File

@ -0,0 +1,21 @@
package data
import "github.com/boltdb/bolt"
// DB is a database for storing information.
var DB *bolt.DB
// UserCred is user credentials for Twitter.
type UserCred struct {
AccessToken string `json:"access"`
SecretToken string `json:"secret"`
}
func init() {
d, err := bolt.Open("db/app.db", 0600, nil)
if err != nil {
panic(err)
}
DB = d
}

0
db/.gitkeep Normal file
View File

4
go.mod
View File

@ -4,9 +4,11 @@ require (
github.com/PuerkitoBio/goquery v1.4.1
github.com/Syfaro/finch v0.0.0-20181005000040-65a305514294
github.com/andybalholm/cascadia v1.0.0 // indirect
github.com/boltdb/bolt v1.3.1
github.com/cenkalti/backoff v2.0.0+incompatible // indirect
github.com/certifi/gocertifi v0.0.0-20180905225744-ee1a9a0726d2 // indirect
github.com/dghubble/go-twitter v0.0.0-20180817201112-a34e9059cd58
github.com/dghubble/oauth1 v0.4.0
github.com/dghubble/sling v1.1.0 // indirect
github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03 // indirect
github.com/go-telegram-bot-api/telegram-bot-api v4.6.3-0.20180922012028-898e79fe47da+incompatible
@ -21,5 +23,5 @@ require (
golang.org/x/oauth2 v0.0.0-20181003184128-c57b0facaced
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e // indirect
huefox.com/syfaro/go-e621 v1.0.0
mvdan.cc/xurls v1.1.0
mvdan.cc/xurls v1.1.1-0.20180901190342-70405f5eab51
)

10
go.sum
View File

@ -6,6 +6,8 @@ github.com/Syfaro/finch v0.0.0-20181005000040-65a305514294 h1:8UnlS1KsRGLiWGHjC1
github.com/Syfaro/finch v0.0.0-20181005000040-65a305514294/go.mod h1:MQMvy4IFRZVDSFhjuAI3FYrvNxcgvKCJNl83wUxCyTI=
github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/cenkalti/backoff v2.0.0+incompatible h1:5IIPUHhlnUZbcHQsQou5k1Tn58nJkeJL9U+ig5CHJbY=
github.com/cenkalti/backoff v2.0.0+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/certifi/gocertifi v0.0.0-20180905225744-ee1a9a0726d2 h1:MmeatFT1pTPSVb4nkPmBFN/LRZ97vPjsFKsZrU3KKTs=
@ -14,6 +16,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dghubble/go-twitter v0.0.0-20180817201112-a34e9059cd58 h1:wnxH1Y1QLw60flg8Cv8nmlnud57a9GqgzOvDMSIm1i4=
github.com/dghubble/go-twitter v0.0.0-20180817201112-a34e9059cd58/go.mod h1:6beqTZaXeBPti9pDBcBEqxfJc7uCbSafqZPRDPQOKoM=
github.com/dghubble/oauth1 v0.4.0 h1:+MpOsgByu02lzT4pRGei5d2p6nAgj8yDwF3RASrgNPQ=
github.com/dghubble/oauth1 v0.4.0/go.mod h1:8V8BMV9DJRREZx/lUaHtrs7GUMXpzbMqJxINCasxYug=
github.com/dghubble/sling v1.1.0 h1:DLu20Bq2qsB9cI5Hldaxj+TMPEaPpPE8IR2kvD22Atg=
github.com/dghubble/sling v1.1.0/go.mod h1:ZcPRuLm0qrcULW2gOrjXrAWgf76sahqSyxXyVOvkunE=
github.com/getsentry/raven-go v0.0.0-20180903072508-084a9de9eb03 h1:G/9fPivTr5EiyqE9OlW65iMRUxFXMGRHgZFGo50uG8Q=
@ -28,6 +32,10 @@ github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f26
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/mvdan/xurls v1.1.0 h1:OpuDelGQ1R1ueQ6sSryzi6P+1RtBpfQHM8fJwlE45ww=
github.com/mvdan/xurls v1.1.0/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
github.com/mvdan/xurls v1.1.1-0.20180901190342-70405f5eab51 h1:sOrGoHmQZ7SOhmPRzivxGfkAvEKWR0Pul+lCSI11sMM=
github.com/mvdan/xurls v1.1.1-0.20180901190342-70405f5eab51/go.mod h1:tQlNn3BED8bE/15hnSL2HLkDeLWpNPAwtw7wkEq44oU=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -57,3 +65,5 @@ huefox.com/syfaro/go-e621 v1.0.0 h1:DJrA8EfkXqzKasB4PKqy36gIONjpB0W5ZeV6Bi3tu3Y=
huefox.com/syfaro/go-e621 v1.0.0/go.mod h1:8E1exlEIJz+yCtdIH6rqPtV9h5TcVTFOpyORBFzcP6A=
mvdan.cc/xurls v1.1.0 h1:kj0j2lonKseISJCiq1Tfk+iTv65dDGCl0rTbanXJGGc=
mvdan.cc/xurls v1.1.0/go.mod h1:TNWuhvo+IqbUCmtUIb/3LJSQdrzel8loVpgFm0HikbI=
mvdan.cc/xurls v1.1.1-0.20180901190342-70405f5eab51 h1:hlah4i0IrBjAiDiEn0sVVk1bi2rHxWPpKcMfAT2Uot0=
mvdan.cc/xurls v1.1.1-0.20180901190342-70405f5eab51/go.mod h1:TNWuhvo+IqbUCmtUIb/3LJSQdrzel8loVpgFm0HikbI=

View File

@ -5,6 +5,7 @@ import (
"strings"
"github.com/Syfaro/finch"
"github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/sirupsen/logrus"
"huefox.com/syfaro/telegram-furryimgbot/logger"
@ -58,7 +59,7 @@ func (direct) IsSupportedURL(url string) bool {
return false
}
func (d direct) GetImageURLs(postURL string) ([]PostInfo, error) {
func (d direct) GetImageURLs(postURL string, _ tgbotapi.User) ([]PostInfo, error) {
return []PostInfo{PostInfo{
FileType: "something",
URL: postURL,

View File

@ -5,6 +5,7 @@ import (
"strings"
"github.com/Syfaro/finch"
"github.com/go-telegram-bot-api/telegram-bot-api"
goe621 "huefox.com/syfaro/go-e621"
)
@ -16,7 +17,7 @@ func (e621) IsSupportedURL(url string) bool {
return goe621.ParsePostURL(url) != nil || goe621.ParseDirectURL(url) != nil
}
func (e e621) GetImageURLs(url string) ([]PostInfo, error) {
func (e e621) GetImageURLs(url string, _ tgbotapi.User) ([]PostInfo, error) {
var post *goe621.Post
var err error

View File

@ -8,6 +8,7 @@ import (
"github.com/PuerkitoBio/goquery"
"github.com/Syfaro/finch"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
type fa struct {
@ -27,7 +28,7 @@ func (fa) IsSupportedURL(url string) bool {
return strings.Contains(url, "furaffinity.net/view/") || strings.Contains(url, "furaffinity.net/full/")
}
func (f fa) GetImageURLs(postURL string) ([]PostInfo, error) {
func (f fa) GetImageURLs(postURL string, _ tgbotapi.User) ([]PostInfo, error) {
u, err := url.Parse(postURL)
if err != nil {
return nil, err

View File

@ -8,6 +8,7 @@ import (
"sync"
"github.com/Syfaro/finch"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
type mastodon struct {
@ -62,7 +63,7 @@ type mastodonUsefulInfo struct {
} `json:"media_attachments"`
}
func (m mastodon) GetImageURLs(postURL string) ([]PostInfo, error) {
func (m mastodon) GetImageURLs(postURL string, _ tgbotapi.User) ([]PostInfo, error) {
match := mastodonExtractor.FindStringSubmatch(postURL)
if match == nil {
return nil, nil

View File

@ -5,14 +5,13 @@ import (
"encoding/hex"
"github.com/Syfaro/finch"
"github.com/go-telegram-bot-api/telegram-bot-api"
)
// Site is a supported site to load an image from.
type Site interface {
IsSupportedURL(url string) bool
GetImageURLs(url string) ([]PostInfo, error)
GetImageURLs(url string, from tgbotapi.User) ([]PostInfo, error)
loadConfig(*finch.Finch)
}

View File

@ -1,11 +1,19 @@
package sites
import (
"encoding/json"
"regexp"
"strconv"
"github.com/Syfaro/finch"
"github.com/boltdb/bolt"
"github.com/go-telegram-bot-api/telegram-bot-api"
"huefox.com/syfaro/telegram-furryimgbot/data"
"huefox.com/syfaro/telegram-furryimgbot/logger"
"github.com/dghubble/go-twitter/twitter"
"github.com/dghubble/oauth1"
twitterOAuth1 "github.com/dghubble/oauth1/twitter"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
)
@ -13,13 +21,18 @@ import (
var twitMatch = regexp.MustCompile(`https?:\/\/twitter\.com\/(?P<username>\w+)\/status\/(?P<id>\d+)`)
type twit struct {
consumerKey, secretKey string
client *twitter.Client
}
func (t *twit) loadConfig(f *finch.Finch) {
t.consumerKey = f.Config.Get("twitter_consumer_key").(string)
t.secretKey = f.Config.Get("twitter_secret_key").(string)
config := &clientcredentials.Config{
ClientID: f.Config.Get("twitter_consumer_key").(string),
ClientSecret: f.Config.Get("twitter_consumer_secret").(string),
ClientID: t.consumerKey,
ClientSecret: t.secretKey,
TokenURL: "https://api.twitter.com/oauth2/token",
}
@ -31,7 +44,7 @@ func (twit) IsSupportedURL(url string) bool {
return twitMatch.MatchString(url)
}
func (t twit) GetImageURLs(postURL string) ([]PostInfo, error) {
func (t twit) GetImageURLs(postURL string, user tgbotapi.User) ([]PostInfo, error) {
match := twitMatch.FindStringSubmatch(postURL)
if match == nil {
return nil, nil
@ -40,13 +53,59 @@ func (t twit) GetImageURLs(postURL string) ([]PostInfo, error) {
tweetIDStr := match[2]
tweetID, _ := strconv.ParseInt(tweetIDStr, 10, 64)
status, _, err := t.client.Statuses.Show(tweetID, &twitter.StatusShowParams{
TweetMode: "extended",
var cred *data.UserCred
err := data.DB.View(func(t *bolt.Tx) error {
b := t.Bucket([]byte("twittercreds"))
if b == nil {
logger.Log.Debug("twitter bucket does not exist")
return nil
}
bytes := b.Get([]byte(strconv.Itoa(user.ID)))
if bytes == nil {
logger.Log.WithField("user_id", user.ID).Debug("do not have credentials for user")
return nil
}
err := json.Unmarshal(bytes, &cred)
if err != nil {
logger.Log.WithField("user_id", user.ID).Debug("unable to get saved credentials")
return err
}
return nil
})
if err != nil {
return nil, err
}
var status *twitter.Tweet
if cred == nil {
status, _, err = t.client.Statuses.Show(tweetID, &twitter.StatusShowParams{
TweetMode: "extended",
})
} else {
config := &oauth1.Config{
ConsumerKey: t.consumerKey,
ConsumerSecret: t.secretKey,
Endpoint: twitterOAuth1.AuthorizeEndpoint,
}
token := oauth1.NewToken(cred.AccessToken, cred.SecretToken)
client := twitter.NewClient(config.Client(oauth1.NoContext, token))
logger.Log.Debug("Using saved Twitter credentials")
status, _, err = client.Statuses.Show(tweetID, &twitter.StatusShowParams{
TweetMode: "extended",
})
}
if err != nil {
return nil, err
}
if status == nil ||
status.ExtendedEntities == nil ||
status.ExtendedEntities.Media == nil ||