Make Blog API use Medium
This commit is contained in:
parent
3961e310b1
commit
ab89bda819
@ -3,10 +3,6 @@ package app
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"zxq.co/ripple/rippleapi/app/internals"
|
|
||||||
"zxq.co/ripple/rippleapi/app/peppy"
|
|
||||||
"zxq.co/ripple/rippleapi/app/v1"
|
|
||||||
"zxq.co/ripple/rippleapi/common"
|
|
||||||
"github.com/DataDog/datadog-go/statsd"
|
"github.com/DataDog/datadog-go/statsd"
|
||||||
"github.com/getsentry/raven-go"
|
"github.com/getsentry/raven-go"
|
||||||
"github.com/gin-gonic/contrib/gzip"
|
"github.com/gin-gonic/contrib/gzip"
|
||||||
@ -14,6 +10,10 @@ import (
|
|||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
"github.com/serenize/snaker"
|
"github.com/serenize/snaker"
|
||||||
"gopkg.in/redis.v5"
|
"gopkg.in/redis.v5"
|
||||||
|
"zxq.co/ripple/rippleapi/app/internals"
|
||||||
|
"zxq.co/ripple/rippleapi/app/peppy"
|
||||||
|
"zxq.co/ripple/rippleapi/app/v1"
|
||||||
|
"zxq.co/ripple/rippleapi/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -115,7 +115,6 @@ func Start(conf common.Conf, dbO *sqlx.DB) *gin.Engine {
|
|||||||
gv1.GET("/users/self", Method(v1.UserSelfGET))
|
gv1.GET("/users/self", Method(v1.UserSelfGET))
|
||||||
gv1.GET("/tokens/self", Method(v1.TokenSelfGET))
|
gv1.GET("/tokens/self", Method(v1.TokenSelfGET))
|
||||||
gv1.GET("/blog/posts", Method(v1.BlogPostsGET))
|
gv1.GET("/blog/posts", Method(v1.BlogPostsGET))
|
||||||
gv1.GET("/blog/posts/content", Method(v1.BlogPostsContentGET))
|
|
||||||
gv1.GET("/scores", Method(v1.ScoresGET))
|
gv1.GET("/scores", Method(v1.ScoresGET))
|
||||||
gv1.GET("/beatmaps/rank_requests/status", Method(v1.BeatmapRankRequestsStatusGET))
|
gv1.GET("/beatmaps/rank_requests/status", Method(v1.BeatmapRankRequestsStatusGET))
|
||||||
|
|
||||||
|
208
app/v1/blog.go
208
app/v1/blog.go
@ -1,17 +1,75 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"zxq.co/ripple/rippleapi/common"
|
"zxq.co/ripple/rippleapi/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type blogPost struct {
|
// This basically proxies requests from Medium's API and is used on Ripple's
|
||||||
ID int `json:"id"`
|
// home page to display the latest blog posts.
|
||||||
|
|
||||||
|
type mediumResp struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Payload struct {
|
||||||
|
Posts []mediumPost `json:"posts"`
|
||||||
|
} `json:"payload"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mediumPost struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Creator mediumUser `json:"creator"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Slug string `json:"slug"`
|
CreatedAt int64 `json:"createdAt"`
|
||||||
Created time.Time `json:"created"`
|
UpdatedAt int64 `json:"updatedAt"`
|
||||||
Author userData `json:"author"`
|
Virtuals mediumPostVirtuals `json:"virtuals"`
|
||||||
|
ImportedURL string `json:"importedUrl"`
|
||||||
|
UniqueSlug string `json:"uniqueSlug"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mediumUser struct {
|
||||||
|
UserID string `json:"userId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mediumPostVirtuals struct {
|
||||||
|
CreatedAtRelative string `json:"createdAtRelative"`
|
||||||
|
UpdatedAtRelative string `json:"updatedAtRelative"`
|
||||||
|
Snippet string `json:"snippet"`
|
||||||
|
WordCount int `json:"wordCount"`
|
||||||
|
ReadingTime float64 `json:"readingTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// there's gotta be a better way
|
||||||
|
|
||||||
|
type blogPost struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Creator blogUser `json:"creator"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
ImportedURL string `json:"imported_url"`
|
||||||
|
UniqueSlug string `json:"unique_slug"`
|
||||||
|
|
||||||
|
CreatedAtRelative string `json:"created_at_relative"`
|
||||||
|
UpdatedAtRelative string `json:"updated_at_relative"`
|
||||||
|
Snippet string `json:"snippet"`
|
||||||
|
WordCount int `json:"word_count"`
|
||||||
|
ReadingTime float64 `json:"reading_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type blogUser struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Username string `json:"username"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type blogPostsResponse struct {
|
type blogPostsResponse struct {
|
||||||
@ -19,80 +77,102 @@ type blogPostsResponse struct {
|
|||||||
Posts []blogPost `json:"posts"`
|
Posts []blogPost `json:"posts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// consts for the medium API
|
||||||
|
const (
|
||||||
|
mediumAPIResponsePrefix = `])}while(1);</x>`
|
||||||
|
mediumAPIAllPosts = `https://blog.ripple.moe/all?format=json`
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register([]blogPost{})
|
||||||
|
}
|
||||||
|
|
||||||
// BlogPostsGET retrieves the latest blog posts on the Ripple blog.
|
// BlogPostsGET retrieves the latest blog posts on the Ripple blog.
|
||||||
func BlogPostsGET(md common.MethodData) common.CodeMessager {
|
func BlogPostsGET(md common.MethodData) common.CodeMessager {
|
||||||
var and string
|
// check if posts are cached in redis
|
||||||
var params []interface{}
|
res := md.R.Get("api:blog_posts").Val()
|
||||||
if md.Query("id") != "" {
|
if res != "" {
|
||||||
and = "b.id = ?"
|
// decode values
|
||||||
params = append(params, md.Query("id"))
|
posts := make([]blogPost, 0, 20)
|
||||||
}
|
err := gob.NewDecoder(strings.NewReader(res)).Decode(&posts)
|
||||||
rows, err := md.DB.Query(`
|
|
||||||
SELECT
|
|
||||||
b.id, b.title, b.slug, b.created,
|
|
||||||
|
|
||||||
u.id, u.username, s.username_aka, u.register_datetime,
|
|
||||||
u.privileges, u.latest_activity, s.country
|
|
||||||
FROM anchor_posts b
|
|
||||||
INNER JOIN users u ON b.author = u.id
|
|
||||||
INNER JOIN users_stats s ON b.author = s.id
|
|
||||||
WHERE status = "published" `+and+`
|
|
||||||
ORDER BY b.id DESC `+common.Paginate(md.Query("p"), md.Query("l"), 50), params...)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
md.Err(err)
|
md.Err(err)
|
||||||
return Err500
|
return Err500
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create response and return
|
||||||
var r blogPostsResponse
|
var r blogPostsResponse
|
||||||
for rows.Next() {
|
r.Code = 200
|
||||||
var post blogPost
|
r.Posts = posts
|
||||||
err := rows.Scan(
|
return r
|
||||||
&post.ID, &post.Title, &post.Slug, &post.Created,
|
}
|
||||||
|
|
||||||
&post.Author.ID, &post.Author.Username, &post.Author.UsernameAKA, &post.Author.RegisteredOn,
|
// get data from medium api
|
||||||
&post.Author.Privileges, &post.Author.LatestActivity, &post.Author.Country,
|
resp, err := http.Get(mediumAPIAllPosts)
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
md.Err(err)
|
md.Err(err)
|
||||||
continue
|
return Err500
|
||||||
}
|
}
|
||||||
r.Posts = append(r.Posts, post)
|
|
||||||
}
|
|
||||||
r.Code = 200
|
|
||||||
|
|
||||||
return r
|
// read body and trim the prefix
|
||||||
}
|
all, err := ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
type blogPostContent struct {
|
|
||||||
common.ResponseBase
|
|
||||||
Content string `json:"content"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// BlogPostsContentGET retrieves the content of a specific blog post.
|
|
||||||
func BlogPostsContentGET(md common.MethodData) common.CodeMessager {
|
|
||||||
field := "markdown"
|
|
||||||
if md.HasQuery("html") {
|
|
||||||
field = "html"
|
|
||||||
}
|
|
||||||
var (
|
|
||||||
by string
|
|
||||||
val string
|
|
||||||
)
|
|
||||||
switch {
|
|
||||||
case md.Query("slug") != "":
|
|
||||||
by = "slug"
|
|
||||||
val = md.Query("slug")
|
|
||||||
case md.Query("id") != "":
|
|
||||||
by = "id"
|
|
||||||
val = md.Query("id")
|
|
||||||
default:
|
|
||||||
return ErrMissingField("id|slug")
|
|
||||||
}
|
|
||||||
var r blogPostContent
|
|
||||||
err := md.DB.QueryRow("SELECT "+field+" FROM anchor_posts WHERE "+by+" = ? AND status = 'published'", val).Scan(&r.Content)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return common.SimpleResponse(404, "no blog post found")
|
md.Err(err)
|
||||||
|
return Err500
|
||||||
}
|
}
|
||||||
|
all = bytes.TrimPrefix(all, []byte(mediumAPIResponsePrefix))
|
||||||
|
|
||||||
|
// unmarshal into response struct
|
||||||
|
var mResp mediumResp
|
||||||
|
err = json.Unmarshal(all, &mResp)
|
||||||
|
if err != nil {
|
||||||
|
md.Err(err)
|
||||||
|
return Err500
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mResp.Success {
|
||||||
|
md.Err(errors.New("medium api call is not successful"))
|
||||||
|
return Err500
|
||||||
|
}
|
||||||
|
|
||||||
|
// create posts slice and fill it up with converted posts from the medium
|
||||||
|
// API
|
||||||
|
posts := make([]blogPost, len(mResp.Payload.Posts))
|
||||||
|
for idx, mp := range mResp.Payload.Posts {
|
||||||
|
var p blogPost
|
||||||
|
|
||||||
|
// convert structs
|
||||||
|
p.ID = mp.ID
|
||||||
|
p.Title = mp.Title
|
||||||
|
p.CreatedAt = time.Unix(0, mp.CreatedAt*1000000)
|
||||||
|
p.UpdatedAt = time.Unix(0, mp.UpdatedAt*1000000)
|
||||||
|
p.ImportedURL = mp.ImportedURL
|
||||||
|
p.UniqueSlug = mp.UniqueSlug
|
||||||
|
|
||||||
|
p.Creator.UserID = mp.Creator.UserID
|
||||||
|
p.Creator.Name = mp.Creator.Name
|
||||||
|
p.Creator.Username = mp.Creator.Username
|
||||||
|
|
||||||
|
p.CreatedAtRelative = mp.Virtuals.CreatedAtRelative
|
||||||
|
p.UpdatedAtRelative = mp.Virtuals.UpdatedAtRelative
|
||||||
|
p.Snippet = mp.Virtuals.Snippet
|
||||||
|
p.WordCount = mp.Virtuals.WordCount
|
||||||
|
p.ReadingTime = mp.Virtuals.ReadingTime
|
||||||
|
|
||||||
|
posts[idx] = p
|
||||||
|
}
|
||||||
|
|
||||||
|
// save in redis
|
||||||
|
bb := new(bytes.Buffer)
|
||||||
|
err = gob.NewEncoder(bb).Encode(posts)
|
||||||
|
if err != nil {
|
||||||
|
md.Err(err)
|
||||||
|
return Err500
|
||||||
|
}
|
||||||
|
md.R.Set("api:blog_posts", bb.Bytes(), time.Minute*5)
|
||||||
|
|
||||||
|
var r blogPostsResponse
|
||||||
r.Code = 200
|
r.Code = 200
|
||||||
|
r.Posts = posts
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user