Skip to content

Commit

Permalink
feat: add Newsletter Scheduling Func
Browse files Browse the repository at this point in the history
  • Loading branch information
slowhigh committed May 31, 2024
1 parent fee194d commit 4d80016
Show file tree
Hide file tree
Showing 9 changed files with 421 additions and 163 deletions.
53 changes: 53 additions & 0 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: cd

on:
push:
branches:
- main

concurrency:
group: cd
cancel-in-progress: true

jobs:
deploy_api_server:
name: Push Docker image to Docker Hub, Deploy API Server.
runs-on: ubuntu-latest

steps:
- name: Check out the repo
uses: actions/checkout@v3
with:
ref: ${{ github.ref_name }}

- name: Set short git commit SHA
id: vars
run: |
calculatedSha=$(git rev-parse --short ${{ github.sha }})
echo "::set-output name=short_sha::$calculatedSha"
- name: Confirm git commit SHA output
run: echo ${{ steps.vars.outputs.short_sha }}

- name: Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/send-server:${{ steps.vars.outputs.short_sha }}

- name: Deploy to K8S
uses: appleboy/[email protected]
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
password: ${{ secrets.PASSWORD }}
port: ${{ secrets.PORT }}
script: kubectl -n ${{ secrets.NAMESPACE }} set image deploy/send-server send-server=${{ secrets.DOCKERHUB_USERNAME }}/send-server:${{ steps.vars.outputs.short_sha }}
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: ci

on:
push:
branches:
- main

concurrency:
group: ci
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: './go.mod'

- name: Build
run: go build -v -o ./server ./cmd/server

- name: Test
run: go test -v ./cmd/server
14 changes: 14 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM golang:alpine

WORKDIR /app

COPY go.mod .
COPY go.sum .

RUN go mod download

COPY . .

RUN go build -o ./server ./cmd/server

CMD [ "./server" ]
16 changes: 16 additions & 0 deletions cmd/server/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"DATABASE": {
"HOST": "localhost",
"PORT": 5432,
"USER_NAME": "nerd",
"PASSWORD": "planet1!",
"DB_NAME": "nerd_planet",
"LOG_LEVEL": 4
},
"SMTP": {
"HOST": "localhost",
"PORT": 5000,
"USER_NAME": "nerd",
"PASSWORD": "planet1!"
}
}
199 changes: 199 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package main

import (
"bytes"
"fmt"
"log"
"log/slog"
"net/smtp"
"os"
"path"
"runtime"
"strconv"
"strings"
"text/template"
"time"

"github.com/jasonlvhit/gocron"
"github.com/spf13/viper"
"github.com/team-nerd-planet/send-server/entity"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)

func main() {
conf, err := NewConfig()
if err != nil {
panic(err)
}

dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable TimeZone=Asia/Seoul",
conf.Database.Host,
conf.Database.Port,
conf.Database.UserName,
conf.Database.Password,
conf.Database.DbName,
)

newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.LogLevel(4), // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: false, // Don't include params in the SQL log
Colorful: false, // Disable color
},
)

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
panic(err)
}

gocron.Every(1).Day().At("09:00").Do(func() {
var subscriptionArr []entity.Subscription

if err := db.Find(&subscriptionArr).Error; err != nil {
panic(err)
}

for _, subscription := range subscriptionArr {
publish(conf, db, subscription)
}
})

<-gocron.Start()
}

func publish(conf *Config, db *gorm.DB, subscription entity.Subscription) {
var (
items []entity.ItemView
where = make([]string, 0)
param = make([]interface{}, 0)
)

if len(subscription.PreferredCompanyArr) > 0 {
where = append(where, "feed_id IN ?")
param = append(param, []int64(subscription.PreferredCompanyArr))
}

if len(subscription.PreferredCompanySizeArr) > 0 {
where = append(where, "company_size IN ?")
param = append(param, []int64(subscription.PreferredCompanySizeArr))
}

if len(subscription.PreferredJobArr) > 0 {
where = append(where, "job_tags_id_arr && ?") // `&&`: overlap (have elements in common)
param = append(param, getArrToString(subscription.PreferredJobArr))
}

if len(subscription.PreferredSkillArr) > 0 {
where = append(where, "skill_tags_id_arr && ?") // `&&`: overlap (have elements in common)
param = append(param, getArrToString(subscription.PreferredSkillArr))
}

if err := db.Select(
"item_title",
"LEFT(item_description, 50) as item_description",
"item_link",
"NULLIF(item_thumbnail, 'https://www.nerdplanet.app/images/feed-thumbnail.png') as item_thumbnail",
"feed_name",
).Where(strings.Join(where, " AND "), param...).Limit(10).Find(&items).Error; err != nil {
slog.Error(err.Error(), "error", err)
return
}

if len(items) > 0 {
_, b, _, _ := runtime.Caller(0)
configDirPath := path.Join(path.Dir(b))
t, err := template.ParseFiles(fmt.Sprintf("%s/template/newsletter.html", configDirPath))
if err != nil {
slog.Error(err.Error(), "error", err)
return
}

var body bytes.Buffer
if err := t.Execute(&body, items); err != nil {
slog.Error(err.Error(), "error", err)
return
}

auth := smtp.PlainAuth("", conf.Smtp.UserName, conf.Smtp.Password, conf.Smtp.Host)
from := conf.Smtp.UserName
to := []string{subscription.Email}
subject := "Subject: 너드플래닛 기술블로그 뉴스레터 \n"
mime := "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n"
msg := []byte(subject + mime + body.String())
err = smtp.SendMail(fmt.Sprintf("%s:%d", conf.Smtp.Host, conf.Smtp.Port), auth, from, to, msg)
if err != nil {
slog.Error(err.Error(), "error", err)
return
}
}

subscription.Published = time.Now()
if err := db.Save(subscription).Error; err != nil {
slog.Error(err.Error(), "error", err)
return
}
}

func getArrToString(arr []int64) string {
strArr := make([]string, len(arr))
for i, v := range arr {
strArr[i] = strconv.FormatInt(v, 10)
}

return fmt.Sprintf("{%s}", strings.Join(strArr, ","))
}

type Config struct {
Database Database `mapstructure:"DATABASE"`
Smtp Smtp `mapstructure:"SMTP"`
}

type Database struct {
Host string `mapstructure:"HOST"`
Port int `mapstructure:"PORT"`
LogLevel int `mapstructure:"LOG_LEVEL"` // 1:Silent, 2:Error, 3:Warn, 4:Info
UserName string `mapstructure:"USER_NAME"`
Password string `mapstructure:"PASSWORD"`
DbName string `mapstructure:"DB_NAME"`
}

type Smtp struct {
Host string `mapstructure:"HOST"`
Port int `mapstructure:"PORT"`
UserName string `mapstructure:"USER_NAME"`
Password string `mapstructure:"PASSWORD"`
}

func NewConfig() (*Config, error) {
_, b, _, _ := runtime.Caller(0)
configDirPath := path.Join(path.Dir(b))

conf := Config{}
viper.SetConfigName("config")
viper.SetConfigType("json")
viper.AddConfigPath(configDirPath)

err := viper.ReadInConfig()
if err != nil {
slog.Error("Read config file.", "err", err)
return nil, err
}

viper.AutomaticEnv()

err = viper.Unmarshal(&conf)
if err != nil {
slog.Error("Unmarshal config file.", "err", err)
return nil, err
}

return &conf, nil
}
47 changes: 3 additions & 44 deletions templdate.html → cmd/server/template/newsletter.html
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ <h1>너드플래닛 기술블로그 뉴스레터</h1>
<div class="content">
<h2>오늘 올라온 글</h2>

{{range .}}
<div class="article">
<a href="{{.ItemLink}}">
<img src="{{.ItemThumbnail}}" alt="Article Image">
Expand All @@ -122,50 +123,8 @@ <h3><a href="{{.ItemLink}}">{{.ItemTitle}}</a></h3>
<p>{{.ItemDescription}}</p>
</div>
</div>

<div class="article">
<a href="https://example.com/article2">
<img src="https://via.placeholder.com/80" alt="Article Image">
</a>
<div>
<h3><a href="https://example.com/article2">Essential Linux Terminal Hacks for Efficiency</a></h3>
<p>Sagar #OpenToWork in FAUN—Developer Community • 4 min read</p>
<p>Tips and tricks in Linux terminal for better productivity and ease of use.</p>
</div>
</div>

<div class="article">
<a href="https://example.com/article3">
<img src="https://via.placeholder.com/80" alt="Article Image">
</a>
<div>
<h3><a href="https://example.com/article3">Why There Are Many Programmers, but There Aren't Many Good Ones</a></h3>
<p>Josef Cruz in Stackademic • 4 min read</p>
<p>I'll put an end to this question once and for all.</p>
</div>
</div>

<div class="article">
<a href="https://example.com/article4">
<img src="https://via.placeholder.com/80" alt="Article Image">
</a>
<div>
<h3><a href="https://example.com/article4">Unlocking Your Tech Wonderland by Combining GitOps, Platforms and AI</a></h3>
<p>Thomas Schuetz in ITNEXT • 11 min read</p>
<p>Why is Kubernetes so complicated, why is the ecosystem so large and how is this related to platforms? Read...</p>
</div>
</div>

<div class="article">
<a href="https://example.com/article5">
<img src="https://via.placeholder.com/80" alt="Article Image">
</a>
<div>
<h3><a href="https://example.com/article5">Automating My Life with Fabric: The Ultimate Guide</a></h3>
<p>Gao Dalie (高達烈) in Level Up Coding • 9 min read</p>
<p>Here's how you can automate your life using Fabric.</p>
</div>
</div>
{{end}}

</div>
<div class="footer">
<p>Stories for <strong>Nerd Planet</strong> <a href="https://nerdplanet.app">@Nerd Planet</a><a href="#">Become a member</a></p>
Expand Down
Loading

0 comments on commit 4d80016

Please sign in to comment.