Commit 85c30f30 authored by Markus Seidl's avatar Markus Seidl

Initial version.

parent a090303c
Pipeline #27 failed with stages
in 18 seconds
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding" addBOMForNewFiles="with NO BOM" />
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="ES6" />
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/rgifbot.iml" filepath="$PROJECT_DIR$/.idea/rgifbot.iml" />
</modules>
</component>
</project>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>
\ No newline at end of file
package bot
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"time"
tb "gopkg.in/tucnak/telebot.v2"
)
type Message struct {
Url string
File *os.File
}
type TelegramConfig struct {
TelegramToken string
KnownIds map[int]bool
Password string
}
var Config TelegramConfig
const ConfigFile = "./config.json"
func CCTelegramLoop(handler func(message Message) error) {
Config = loadConfig()
if Config.KnownIds == nil {
Config.KnownIds = make(map[int]bool) // necessary?!
}
b, err := tb.NewBot(tb.Settings{
Token: Config.TelegramToken,
Poller: &tb.LongPoller{Timeout: 10 * time.Second},
})
if err != nil {
log.Fatal(err)
return
}
b.Handle(tb.OnText, func(m *tb.Message) {
if !isAllowed(b, m) {
return
}
log.Printf("Received message <%s>, <%d>", m.Text, len(m.Entities))
text := m.Text
if strings.HasPrefix(text, "http") {
err := handler(Message{Url: text})
SendMediaResponse(err, b, m)
return
}
_, _ = b.Send(m.Sender, "Please send me gifs, videos or 9Gag links 🤡")
})
b.Handle(tb.OnVideo, func(m *tb.Message) {
if !isAllowed(b, m) {
return
}
f, err := ioutil.TempFile("", "telegram_file")
if m.Video.InCloud() {
err = b.Download(&m.Video.File, f.Name())
if err != nil {
SendMediaResponse(err, b, m)
return
}
} else {
SendMediaResponse(errors.New("UnknownMediaLocation"), b, m)
return
}
err = handler(Message{Url: "", File: f})
SendMediaResponse(err, b, m)
})
b.Handle("/exit", func(m *tb.Message) {
if !isAllowed(b, m) {
return
}
os.Exit(1)
})
b.Handle("/password", func(m *tb.Message) {
log.Printf("Login attempt: %s, payload: %s", m.Text, m.Payload)
pass := m.Payload
if pass != Config.Password {
_, _ = b.Send(m.Sender, "Wrong!")
return
}
Config.KnownIds[m.Sender.ID] = true
_, _ = b.Send(m.Sender, "Welcome back! 😍")
storeConfig(Config)
})
b.Start()
}
func SendMediaResponse(err error, b *tb.Bot, m *tb.Message) {
if err == nil {
_, _ = b.Send(m.Sender, "Understood 🤩.")
} else {
_, _ = b.Send(m.Sender, fmt.Sprintf("No clue... 😒 <%s>", err.Error()))
}
}
func isAllowed(b *tb.Bot, m *tb.Message) bool {
if !m.Private() {
_, _ = b.Send(m.Sender, "Only private chats are allowed.")
}
id := m.Sender.ID
log.Printf("Current sender <%d, %s>, Known senders <%v>", m.Sender.ID, m.Sender.Username, Config.KnownIds)
if Config.KnownIds[id] {
return true
}
if len(Config.KnownIds) == 0 {
_, _ = b.Send(m.Sender, "I've just awoken from a long nicker...")
}
_, _ = b.Send(m.Sender, "I don't know you (yet). Please type /password <pass>")
return false
}
func loadConfig() TelegramConfig {
configFileData, _ := ioutil.ReadFile(ConfigFile)
var config TelegramConfig
err := json.Unmarshal(configFileData, &config)
if err != nil {
storeConfig(TelegramConfig{"token", make(map[int]bool), ""})
log.Fatalf("Unable to load configfile <%s>. Writing empty file.\n", err.Error())
}
return config
}
func storeConfig(config TelegramConfig) {
m, _ := json.Marshal(config)
err := ioutil.WriteFile(ConfigFile, m, 0666)
if err != nil {
log.Printf("Unable to save configfile <%s>\n", err.Error())
}
}
package display
import (
"log"
"os/exec"
"rgifbot/mplayer"
"rgifbot/simpledb"
"runtime"
"time"
)
func DisplayLoop(db *simpledb.Database) {
var addArgs []string
addArgs = append(addArgs, "-nosound")
if runtime.GOOS == "linux" {
addArgs = append(addArgs, "-vo", "directfb", "-zoom")
}
mplayer.StartSlave(func(err error) {
log.Printf("Error while communicating with mplayer <%v>", err)
}, addArgs...)
go func() {
curvid := db.Rnd() // start with a random file
for {
if curvid == "" {
time.Sleep(time.Millisecond * 500)
} else {
execute(curvid)
}
curvid = db.Next(curvid)
}
}()
for {
time.Sleep(time.Millisecond * 500)
}
}
func execute(file string) {
mplayer.PlayAndWait(file)
}
func executeSimple(file string) {
var args []string
// args = append(args, "mplayer")
app := "/usr/local/bin/mplayer"
if runtime.GOOS == "linux" {
args = append(args, "-vo", "directfb", "-zoom")
}
args = append(args, "\""+file+"\"")
cmd := &exec.Cmd{
Path: app,
Args: args,
}
log.Printf("Executing: %s %v", app, args)
temp, err := cmd.CombinedOutput()
log.Print(err)
log.Print(string(temp))
/*var e bytes.Buffer
cmd.Stderr = &e
stdout, err := cmd.Output()
if err != nil {
log.Printf("Fatal when executing cmd %s", err.Error())
return
}
print(string(stdout))
print(string(e.Bytes()))*/
}
module rgifbot
require (
github.com/pkg/errors v0.8.0
gopkg.in/tucnak/telebot.v2 v2.0.0-20181213002246-6451db130825
)
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=
gopkg.in/tucnak/telebot.v2 v2.0.0-20181213002246-6451db130825 h1:wcmZZzTudW+TVMQvA2nSdOemWGgo5LO5WDxrLu7/PZk=
gopkg.in/tucnak/telebot.v2 v2.0.0-20181213002246-6451db130825/go.mod h1:EmWSFwexO5JPTHiU7gDd9HMCCiyatACwO0HhuKNsOd0=
package main
import (
"errors"
"log"
"os"
"path/filepath"
"rgifbot/bot"
"rgifbot/display"
"rgifbot/scrapper"
"rgifbot/simpledb"
"time"
)
func main() {
dir, _ := filepath.Abs("./data")
db := simpledb.Database{Directory: dir}
go func() {
bot.CCTelegramLoop(func(message bot.Message) (err error) {
var file *os.File
if message.Url != "" {
file, err = scrapper.Scrap(message.Url)
if err != nil {
return
}
} else if message.File != nil {
file = message.File
} else {
return errors.New("UnknownMessageType")
}
defer file.Close()
err = db.Store("", time.Now(), file)
if err != nil {
return err
}
return nil
})
}()
go func() {
display.DisplayLoop(&db)
}()
// prevent app from terminating
log.Println("Initialized.")
for {
time.Sleep(time.Millisecond * 500)
}
}
// Copyright 2014, Bertrand Janin <b@janin.com>. All Rights Reserved.
// Use of this source code is governed by the ISC license in the LICENSE file.
//
// All the commands available to an MPlayer slave process are available in the
// MPlayer docs folder, also available online:
//
// http://www.mplayerhq.hu/DOCS/tech/slave.txt
//
package mplayer
import (
"bufio"
"io"
"log"
"os/exec"
"runtime"
"strings"
"time"
)
type ErrorHandler func(err error)
var (
// Input is used to feed the slave subprocess commands.
Input = make(chan string)
// MPlayer has stopped playing.
stoppedCh = make(chan bool)
// hasStopSignalListeners signals that there are listened for the Stop
// signal emitted
hasStopSignalListeners = false
)
// readOutput is a go routine transferring the stdout of MPlayer to a proper
// channel.
func readOutput(reader io.Reader) {
bufReader := bufio.NewReader(reader)
for {
msg, err := bufReader.ReadString('\n')
if err != nil {
// process likely died, let the routine die as well
log.Println("Subprocess died.")
return
}
msg = strings.TrimSpace(msg)
// When PlayAndWait is used, we send a "get_property path" to
// MPlayer every seconds. If the response is ever that nothing
// is player anymore, we shutdown the player.
if hasStopSignalListeners && msg == "ANS_path=(null)" {
stoppedCh <- true
}
}
}
// Run a single slave process and wait for its completion before returning. If
// the process starts properly, this function will pass to its stdin all the
// message on the Input channel.
func runProcess(addArgs []string) error {
args := append(addArgs, "-quiet", "-slave", "-idle")
app := "mplayer"
if runtime.GOOS == "linux" {
app = "/usr/bin/mplayer"
}
cmd := exec.Command(app, args...)
writer, err := cmd.StdinPipe()
if err != nil {
return err
}
reader, err := cmd.StdoutPipe()
if err != nil {
return err
}
err = cmd.Start()
if err != nil {
return err
}
go readOutput(reader)
for msg := range Input {
_, err = writer.Write([]byte(msg + "\n"))
if err != nil {
break
}
}
err = cmd.Wait()
return err
}
// keepSlaveAlive is a loop keeping at least one instance of MPlayer going
// continuously. If a process exists a new one is created, possibly with a
// delay if the previous one exited with an error. If you want some reporting
// to happen, you need to define an error handler.
func keepSlaveAlive(addArgs []string, errorHandler ErrorHandler) {
for {
err := runProcess(addArgs)
if hasStopSignalListeners {
stoppedCh <- true
}
if err != nil {
if errorHandler != nil {
errorHandler(err)
}
time.Sleep(10 * time.Second)
}
}
}
// StartSlave keeps an mplayer process in slave mode open for the rest of time,
// consuming input from the mplayerInput channel and feeding it to the stdin of
// the process. If the process dies it is restarted automatically.
//
// You are required to define an error handler function that will be called
// with all the errors that could have occured managing the slave.
func StartSlave(errorHandler ErrorHandler, addArgs ...string) {
arrAddArgs := append([]string{}, addArgs...)
go keepSlaveAlive(arrAddArgs, errorHandler)
}
// SendCommand feeds the MPlayer slave with input commands.
func SendCommand(msg string) {
Input <- msg
}
// Copyright 2014, Bertrand Janin <b@janin.com>. All Rights Reserved.
// Use of this source code is governed by the ISC license in the LICENSE file.
//
// All the commands available to an MPlayer slave process are available in the
// MPlayer docs folder, also available online:
//
// http://www.mplayerhq.hu/DOCS/tech/slave.txt
//
package mplayer
import (
"time"
)
var (
// Skip is a channel used to interrupt an existing playback.
skipCh = make(chan bool)
)
// Skip attempts to cancel an existing PlayAndWait.
func Skip() {
skipCh <- true
}
// PlayAndWait loads the given file and block until the file is done playing.
func PlayAndWait(path string) {
SendCommand("loadfile \"" + path + "\"")
hasStopSignalListeners = true
// Send a query for the path every seconds. The response is expected in
// handleOutput.
ticker := time.Tick(time.Second)
for {
select {
case <-stoppedCh:
hasStopSignalListeners = false
return
case <-ticker:
SendCommand("get_property path")
case <-skipCh:
SendCommand("stop")
hasStopSignalListeners = false
return
}
}
}
// PlayAndWaitWithDuration loads the given file and block until the file is
// done playing. This function will also stop playing after the given duration.
func PlayAndWaitWithDuration(path string, duration time.Duration) {
go func() {
time.Sleep(duration)
SendCommand("stop")
}()
PlayAndWait(path)
}
package scrapper
import (
"github.com/pkg/errors"
"os"
"strings"
)
func Scrap(url string) (file *os.File, err error) {
switch {
case strings.HasPrefix(url, "https://9gag.com") ||
strings.HasPrefix(url, "http://9gag.com"):
return ScrapFromUrl9Gag(url)
}
return nil, errors.New("No scrapper for url <" + url + ">.")
}
package scrapper
import (
"io"
"io/ioutil"
"net/http"
"os"
"strings"
)
func ScrapFromUrl9Gag(url string) (file *os.File, err error) {
// https://9gag.com/gag/aqK747L
url = strings.Replace(url, "https://9gag.com/gag/", "https://img-9gag-fun.9cache.com/photo/", 1)
url = strings.Replace(url, "http://9gag.com/gag/", "https://img-9gag-fun.9cache.com/photo/", 1)
url += "_460sv.mp4"
file, err = ioutil.TempFile("", "9gag_scrapper_")
if err != nil {
return
}
err = DownloadFile(file, url)
return
}
// DownloadFile will download a url to a local file. It's efficient because it will
// write as it downloads and not load the whole file into memory.
func DownloadFile(file *os.File, url string) error {
// Get the data
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Write the body to file
_, err = io.Copy(file, resp.Body)
if err != nil {
return err
}
_ = file.Sync()
return nil
}
package simpledb
import (
"fmt"
"io"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"sort"
"strconv"
"time"
)
const MaxFilesPerDirectory = 100
const DefaultCategory = "default"
type Database struct {
Directory string
}
func (d *Database) Store(category string, time time.Time, input *os.File) error {
if category == "" {
category = DefaultCategory
}
var dirs []int
// find latest XX dir
files, _ := ioutil.ReadDir(d.Directory + "/" + category + "/")
for _, element := range files {
_, name := filepath.Split(element.Name())
dir, err := strconv.Atoi(name)
if err != nil {
continue
}
dirs = append(dirs, dir)
}
lastNo := 0
if len(dirs) >= 1 {
sort.Ints(dirs)
lastNo = dirs[len(dirs)-1]
// if it's full
temp, _ := ioutil.ReadDir(d.createDirectoryFor(category, lastNo))
if len(temp) > MaxFilesPerDirectory {
lastNo++
}
}
baseDir := d.createDirectoryFor(category, lastNo)
_ = os.MkdirAll(baseDir, os.ModePerm)
// write input to the new location
_, _ = input.Seek(0, 0) // seek to the beginning
output, err := os.Create(baseDir + fmt.Sprintf("%d", time.UnixNano()))
if err != nil {
return err
}
_, err = io.Copy(output, input)
return err
}
func (d *Database) createDirectoryFor(category string, id int) string {
return fmt.Sprintf("%s/%s/%02d/", d.Directory, category, id)
}
func (d *Database) AllFiles() []string {
var files []string
_ = filepath.Walk(d.Directory, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
files = append(files, path)
return nil
})
return files
}
func (d *Database) Next(baseFile string) string {
files := d.AllFiles()
if baseFile == "" {
if len(files) > 0 {
return files[0]
}
return ""
}
_, baseName := filepath.Split(baseFile)
baseInt, _ := strconv.Atoi(baseName)
// flaky, this might not find the real next every time
for _, e := range files {
_, filename := filepath.Split(e)
unixtime, _ := strconv.Atoi(filename)
if unixtime > baseInt {
return e
}
}
if len(files) > 0 {
return files[0]
}
return ""
}
func (d *Database) Rnd() string {
allFiles := d.AllFiles()
if len(allFiles) == 0 {
return ""
}
return allFiles[rand.Intn(len(allFiles))]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment