Skip to content

Commit

Permalink
initial publisher app #1
Browse files Browse the repository at this point in the history
  • Loading branch information
umputun committed Feb 4, 2020
1 parent 306bf98 commit 86e0788
Show file tree
Hide file tree
Showing 85 changed files with 15,593 additions and 0 deletions.
10 changes: 10 additions & 0 deletions publisher/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
FROM umputun/baseimage:buildgo-latest as build

ADD app /build
WORKDIR /build
RUN go test -mod=vendor ./...
RUN CGO_ENABLED=0 GOOS=linux go build -mod=vendor -o /target/publisher -ldflags \
"-X main.revision=$(git rev-parse --abbrev-ref HEAD)-$(git describe --abbrev=7 --always --tags)-$(date +%Y%m%d-%H:%M:%S)"


FROM umputun/baseimage:app-latest

RUN \
Expand All @@ -13,6 +22,7 @@ RUN \
chmod 600 /home/app/.ssh/* && \
chmod 700 /home/app/.ssh

COPY --from=build /target/publisher /srv/publisher/publisher
COPY . /srv/publisher
WORKDIR /srv/publisher

Expand Down
69 changes: 69 additions & 0 deletions publisher/app/cmd/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package cmd

import (
"encoding/json"
"fmt"
"net/http"
"os/exec"

log "github.com/go-pkgz/lgr"
"github.com/pkg/errors"
)

type Executor interface {
Do(cmd string) error
Run(cmd string, params ...interface{})
}

// LastShow get the number of latest published podcast via site-api
// GET /last/{posts}?categories=podcast
func LastShow(client http.Client, siteAPI string) (int, error) {
resp, err := client.Get(fmt.Sprintf("%s/last/1?categories=podcast", siteAPI))
if err != nil {
return -1, errors.Wrap(err, "can't get last shows")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return -1, errors.Errorf("invalid status code %s", resp.Status)
}

//noinspection GoPreferNilSlice
showInfo := []struct {
Num int `json:"show_num"`
}{}

if err = json.NewDecoder(resp.Body).Decode(&showInfo); err != nil {
return -1, errors.Wrap(err, "can't read and decode")
}

if len(showInfo) < 1 {
return -1, errors.New("list of podcasts is empty")
}

return showInfo[0].Num, nil
}

// ShellExecutor is a simple wrapper to execute command within shell
type ShellExecutor struct {
Dry bool
}

// Do executes command and returns error if failed
func (c *ShellExecutor) Do(cmd string) error {
log.Printf("[DEBUG] execute %q", cmd)
if c.Dry {
return nil
}
ex := exec.Command("sh", "-c", cmd)
ex.Stdout = log.ToWriter(log.Default(), "INFO")
ex.Stderr = log.ToWriter(log.Default(), "WARN")
return errors.Wrapf(ex.Run(), "failed to run %q", cmd)
}

// Run makes the final command in printf style and panic on error
func (c *ShellExecutor) Run(cmd string, params ...interface{}) {
command := fmt.Sprintf(cmd, params...)
if err := c.Do(command); err != nil {
log.Fatalf("[ERROR] %v", err)
}
}
47 changes: 47 additions & 0 deletions publisher/app/cmd/cmd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package cmd

import (
"net/http"
"net/http/httptest"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestCmd_LastShow(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/last/1?categories=podcast", r.URL.String())
w.Write([]byte(`[{"show_num": 683}]`))
}))
defer ts.Close()

res, err := LastShow(http.Client{Timeout: 10 * time.Millisecond}, ts.URL)
require.NoError(t, err)
assert.Equal(t, 683, res)
}

func TestCmd_LastShowFailed(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/last/1?categories=podcast", r.URL.String())
w.Write([]byte(`[]`))
}))
defer ts.Close()

_, err := LastShow(http.Client{Timeout: 10 * time.Millisecond}, "http://127.0.0.2:9999/xyz")
require.Error(t, err)
assert.Contains(t, err.Error(), "can't get last shows")
}

func TestShellExecutor_Do(t *testing.T) {
c := ShellExecutor{}
err := c.Do("ls -la")
assert.NoError(t, err)

err = c.Do("ls -la && pwd")
assert.NoError(t, err)

err = c.Do("lxxxxxxs -la")
assert.Error(t, err)
}
59 changes: 59 additions & 0 deletions publisher/app/cmd/deploy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package cmd

import (
"fmt"
"net/http"
"time"

log "github.com/go-pkgz/lgr"
"github.com/pkg/errors"
)

// Deploy delivers site update
type Deploy struct {
Executor Executor
NewsPasswd string
NewsAPI string
NewsDuration time.Duration
Client http.Client
Dry bool
}

// Do run deploy sequence for the given episodeNum
// may panic on executor error
func (d *Deploy) Do(episodeNum int) error {
log.Printf("[INFO] commit new episode to git")
d.Executor.Run("git pull && git commit -am episode %d && git push", episodeNum)

log.Printf("[INFO] remote site update")
d.Executor.Run(`ssh [email protected] "cd /srv/site.hugo && git pull && docker-compose run --rm hugo"`)

log.Printf("[INFO] create chat log")
d.Executor.Run(`ssh [email protected] "docker exec -i super-bot /srv/telegram-rt-bot --super=umputun --super=bobuk --super=ksenks --super=grayru --dbg --export-num=%d --export-path=/srv/html"`, episodeNum)

log.Printf("[INFO] archive news")
err := d.archiveNews()
return err
}

func (d *Deploy) archiveNews() error {
req, err := http.NewRequest("DELETE", fmt.Sprintf("%s/active/last/%d", d.NewsAPI, int(d.NewsDuration.Hours())), nil)
if err != nil {
return errors.Wrap(err, "failed to prepare news archive request")
}
if d.Dry {
log.Printf("[INFO] %s", req.URL.String())
return nil
}

req.SetBasicAuth("admin", d.NewsPasswd)
resp, err := d.Client.Do(req)
if err != nil {
return errors.Wrap(err, "can't make news archive request")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.Wrapf(err, "news archive request returned %s", resp.Status)
}
return nil
}
121 changes: 121 additions & 0 deletions publisher/app/cmd/prep.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package cmd

import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"text/template"
"time"

"github.com/pkg/errors"
)

var newShowTmpl = `+++
title = "Радио-Т {{.EpisodeNum}}"
date = {{.TS.Format "2006-01-02T15:04:05"}}
categories = ["podcast"]
image = "https://radio-t.com/images/radio-t/rt{{.EpisodeNum}}.jpg"
filename = "rt_podcast{{.EpisodeNum}}"
+++
![](https://radio-t.com/images/radio-t/rt{{.EpisodeNum}}.jpg)
{{.News}}
*Спонсор этого выпуска [DigitalOcean](https://www.digitalocean.com)*
[аудио](https://cdn.radio-t.com/rt_podcast{{.EpisodeNum}}.mp3) • [лог чата](https://chat.radio-t.com/logs/radio-t-{{.EpisodeNum}}.html)
<audio src="https://cdn.radio-t.com/rt_podcast{{.EpisodeNum}}.mp3" preload="none"></audio>
`

var prepShowTmpl = `+++
title = "Темы для {{.EpisodeNum}}"
date = {{.TS.Format "2006-01-02T15:04:05"}}
categories = ["prep"]
+++
`

// Prep implements both preparation of md file for the new podcast and for prep-show post
type Prep struct {
Client http.Client
NewsDuration time.Duration
NewsAPI string
Dest string
Dry bool

now func() time.Time
}

// MakeShow creates md file like podcast-123.md based on newShowTmpl and populated from news response
func (p *Prep) MakeShow(episodeNum int) (err error) {
if p.now == nil {
p.now = time.Now
}

tp := struct {
EpisodeNum int
TS time.Time
News string
}{
EpisodeNum: episodeNum,
TS: p.now(),
}

if tp.News, err = p.lastNews(int(p.NewsDuration.Hours())); err != nil {
return errors.Wrap(err, "failed to load last news")
}

return p.applyTemplate(fmt.Sprintf("%s/podcast-%d.md", p.Dest, episodeNum), newShowTmpl, tp)
}

// MakePrep creates a post for news collection, i.e. prep-123.md
func (p *Prep) MakePrep(episodeNum int) (err error) {
if p.now == nil {
p.now = time.Now
}

tp := struct {
EpisodeNum int
TS time.Time
}{
EpisodeNum: episodeNum,
TS: p.now(),
}

return p.applyTemplate(fmt.Sprintf("%s/prep-%d.md", p.Dest, episodeNum), prepShowTmpl, tp)
}

// applyTemplate writes the applied template to outFile
func (p *Prep) applyTemplate(outFile string, tmpl string, tp interface{}) error {
t, err := template.New("tmpl").Parse(tmpl)
if err != nil {
return errors.Wrapf(err, "can't parse template")
}
msg := bytes.Buffer{}
if err = t.Execute(&msg, tp); err != nil {
return errors.Wrapf(err, "can't apply template")
}
if p.Dry {
log.Printf(msg.String())
return nil
}
return errors.Wrapf(ioutil.WriteFile(outFile, msg.Bytes(), 0660), "can't write %s", outFile)
}

// lastNews gets news from news API for the lase hrs hours
func (p *Prep) lastNews(hrs int) (string, error) {
resp, err := p.Client.Get(fmt.Sprintf("%s/lastmd/%d", p.NewsAPI, hrs))
if err != nil {
return "", errors.Wrap(err, "can't get news")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.Errorf("invalid status code %s", resp.Status)
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "can't read news body")
}
return string(b), nil
}
77 changes: 77 additions & 0 deletions publisher/app/cmd/prep_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmd

import (
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestPrep_MakeShow(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/lastmd/12", r.URL.Path)
w.Write([]byte("- blah1\n- blah2"))
}))
defer ts.Close()

p := Prep{
Client: http.Client{Timeout: 100 * time.Millisecond},
NewsDuration: 12 * time.Hour,
NewsAPI: ts.URL,
Dest: "/tmp",
now: func() time.Time { return time.Date(2020, 2, 3, 20, 18, 53, 0, time.Local) },
}

err := p.MakeShow(123)
require.NoError(t, err)
defer os.Remove("/tmp/podcast-123.md")

b, err := ioutil.ReadFile("/tmp/podcast-123.md")
require.NoError(t, err)
exp := `+++
title = "Радио-Т 123"
date = 2020-02-03T20:18:53
categories = ["podcast"]
image = "https://radio-t.com/images/radio-t/rt123.jpg"
filename = "rt_podcast123"
+++
![](https://radio-t.com/images/radio-t/rt123.jpg)
- blah1
- blah2
*Спонсор этого выпуска [DigitalOcean](https://www.digitalocean.com)*
[аудио](https://cdn.radio-t.com/rt_podcast123.mp3) • [лог чата](https://chat.radio-t.com/logs/radio-t-123.html)
<audio src="https://cdn.radio-t.com/rt_podcast123.mp3" preload="none"></audio>
`
assert.Equal(t, exp, string(b))
}

func TestPrep_MakePrep(t *testing.T) {
p := Prep{
Client: http.Client{Timeout: 100 * time.Millisecond},
NewsDuration: 12 * time.Hour,
Dest: "/tmp",
now: func() time.Time { return time.Date(2020, 2, 3, 20, 18, 53, 0, time.Local) },
}

err := p.MakePrep(123)
require.NoError(t, err)
defer os.Remove("/tmp/prep-123.md")

b, err := ioutil.ReadFile("/tmp/prep-123.md")
require.NoError(t, err)
exp := `+++
title = "Темы для 123"
date = 2020-02-03T20:18:53
categories = ["prep"]
+++
`
assert.Equal(t, exp, string(b))
}
Loading

0 comments on commit 86e0788

Please sign in to comment.