Feeds service (#24)

This commit is contained in:
Janos Dobronszki
2020-11-11 17:02:39 +01:00
committed by GitHub
parent b30adabfc3
commit 7a858a62dc
17 changed files with 731 additions and 69 deletions

2
blog/feeds/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
feeds

3
blog/feeds/Dockerfile Normal file
View File

@@ -0,0 +1,3 @@
FROM alpine
ADD feeds /feeds
ENTRYPOINT [ "/feeds" ]

22
blog/feeds/Makefile Normal file
View File

@@ -0,0 +1,22 @@
GOPATH:=$(shell go env GOPATH)
.PHONY: init
init:
go get -u github.com/golang/protobuf/proto
go get -u github.com/golang/protobuf/protoc-gen-go
go get github.com/micro/micro/v3/cmd/protoc-gen-micro
.PHONY: proto
proto:
protoc --proto_path=. --micro_out=. --go_out=:. proto/feeds.proto
.PHONY: build
build:
go build -o feeds *.go
.PHONY: test
test:
go test -v ./... -cover
.PHONY: docker
docker:
docker build . -t feeds:latest

56
blog/feeds/README.md Normal file
View File

@@ -0,0 +1,56 @@
# Feeds Service
This is the Feeds service
Generated with
```
micro new feeds
```
## Usage
```
micro feeds new --name="az" --address=http://a16z.com/feed/
```
```
$ micro posts query
{
"posts": [
{
"id": "39cdfbd6e7534bcd868be9eebbf43f8f",
"title": "Anthony Albanese: From the NYSE to Crypto",
"slug": "anthony-albanese-from-the-nyse-to-crypto",
"created": "1605104742",
"updated": "1605105364",
"metadata": {
"domain": "a16z.com",
"link": "https://a16z.com/2020/10/28/anthony-albanese-from-the-nyse-to-crypto/"
}
},
{
"id": "5e9285c01311704e204322ba564cd99e",
"title": "Journal Club: From Insect Eyes to Nanomaterials",
"slug": "journal-club-from-insect-eyes-to-nanomaterials",
"created": "1605104741",
"updated": "1605105363",
"metadata": {
"domain": "a16z.com",
"link": "https://a16z.com/2020/10/29/journal-club-insect-eyes-nanomaterials/"
}
},
]
}
```
```
make proto
```
Run the service
```
micro run .
```

2
blog/feeds/generate.go Normal file
View File

@@ -0,0 +1,2 @@
package main
//go:generate make proto

View File

@@ -0,0 +1,70 @@
package handler
import (
"context"
"crypto/md5"
"fmt"
"net/url"
"github.com/SlyMarbo/rss"
log "github.com/micro/micro/v3/service/logger"
feeds "github.com/micro/services/blog/feeds/proto"
posts "github.com/micro/services/blog/posts/proto"
)
func (e *Feeds) fetchAll() {
fs := []*feeds.Feed{}
err := e.feeds.List(e.feedsNameIndex.ToQuery(nil), &fs)
if err != nil {
log.Errorf("Error listing feeds: %v", err)
return
}
if len(fs) == 0 {
log.Infof("No feeds to fetch")
return
}
for _, feed := range fs {
log.Infof("Fetching address %v", feed.Address)
fd, err := rss.Fetch(feed.Address)
if err != nil {
log.Errorf("Error fetching address %v: %v", feed.Address, err)
continue
}
domain := getDomain(feed.Address)
for _, item := range fd.Items {
id := fmt.Sprintf("%x", md5.Sum([]byte(item.ID)))
err = e.entries.Save(feeds.Entry{
Id: id,
Url: item.Link,
Title: item.Title,
Domain: domain,
Content: item.Summary,
Date: item.Date.Unix(),
})
if err != nil {
log.Errorf("Error saving item: %v", err)
}
// @todo make this optional
_, err := e.postsService.Save(context.TODO(), &posts.SaveRequest{
Id: id,
Title: item.Title,
Content: item.Content,
Timestamp: item.Date.Unix(),
Metadata: map[string]string{
"domain": domain,
"link": item.Link,
},
})
if err != nil {
log.Errorf("Error saving post: %v", err)
}
}
}
}
func getDomain(address string) string {
uri, _ := url.Parse(address)
return uri.Host
}

View File

@@ -0,0 +1,78 @@
package handler
import (
"context"
"time"
"github.com/micro/dev/model"
log "github.com/micro/micro/v3/service/logger"
"github.com/micro/micro/v3/service/store"
feeds "github.com/micro/services/blog/feeds/proto"
posts "github.com/micro/services/blog/posts/proto"
)
type Feeds struct {
feeds model.Model
entries model.Model
postsService posts.PostsService
feedsIdIndex model.Index
feedsNameIndex model.Index
entriesDateIndex model.Index
}
func NewFeeds(postsService posts.PostsService) *Feeds {
idIndex := model.ByEquality("address")
idIndex.Order.Type = model.OrderTypeUnordered
nameIndex := model.ByEquality("name")
nameIndex.Unique = true
nameIndex.Order.Type = model.OrderTypeUnordered
dateIndex := model.ByEquality("date")
dateIndex.Order.Type = model.OrderTypeDesc
f := &Feeds{
feeds: model.New(
store.DefaultStore,
"feeds",
model.Indexes(nameIndex),
&model.ModelOptions{
Debug: false,
IdIndex: idIndex,
},
),
entries: model.New(
store.DefaultStore,
"entries",
model.Indexes(dateIndex),
&model.ModelOptions{
Debug: false,
},
),
postsService: postsService,
feedsIdIndex: idIndex,
feedsNameIndex: nameIndex,
entriesDateIndex: dateIndex,
}
go f.crawl()
return f
}
func (e *Feeds) crawl() {
e.fetchAll()
tick := time.NewTicker(1 * time.Hour)
for _ = range tick.C {
e.fetchAll()
}
}
func (e *Feeds) New(ctx context.Context, req *feeds.NewRequest, rsp *feeds.NewResponse) error {
log.Info("Received Feeds.New request")
e.feeds.Save(feeds.Feed{
Name: req.Name,
Address: req.Address,
})
return nil
}

27
blog/feeds/main.go Normal file
View File

@@ -0,0 +1,27 @@
package main
import (
pb "github.com/micro/services/blog/feeds/proto"
posts "github.com/micro/services/blog/posts/proto"
"github.com/micro/services/blog/feeds/handler"
"github.com/micro/micro/v3/service"
"github.com/micro/micro/v3/service/logger"
)
func main() {
// Create service
srv := service.New(
service.Name("feeds"),
service.Version("latest"),
)
// Register handler
pb.RegisterFeedsHandler(srv.Server(), handler.NewFeeds(posts.NewPostsService("posts", srv.Client())))
// Run service
if err := srv.Run(); err != nil {
logger.Fatal(err)
}
}

1
blog/feeds/micro.mu Normal file
View File

@@ -0,0 +1 @@
service feeds

View File

@@ -0,0 +1,257 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: proto/feeds.proto
package feeds
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
type Feed struct {
// rss feed name
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
// rss feed address
Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Feed) Reset() { *m = Feed{} }
func (m *Feed) String() string { return proto.CompactTextString(m) }
func (*Feed) ProtoMessage() {}
func (*Feed) Descriptor() ([]byte, []int) {
return fileDescriptor_dd517c38176c13bf, []int{0}
}
func (m *Feed) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Feed.Unmarshal(m, b)
}
func (m *Feed) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Feed.Marshal(b, m, deterministic)
}
func (m *Feed) XXX_Merge(src proto.Message) {
xxx_messageInfo_Feed.Merge(m, src)
}
func (m *Feed) XXX_Size() int {
return xxx_messageInfo_Feed.Size(m)
}
func (m *Feed) XXX_DiscardUnknown() {
xxx_messageInfo_Feed.DiscardUnknown(m)
}
var xxx_messageInfo_Feed proto.InternalMessageInfo
func (m *Feed) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *Feed) GetAddress() string {
if m != nil {
return m.Address
}
return ""
}
type Entry struct {
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Domain string `protobuf:"bytes,2,opt,name=domain,proto3" json:"domain,omitempty"`
Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"`
Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"`
Content string `protobuf:"bytes,5,opt,name=content,proto3" json:"content,omitempty"`
Date int64 `protobuf:"varint,6,opt,name=date,proto3" json:"date,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *Entry) Reset() { *m = Entry{} }
func (m *Entry) String() string { return proto.CompactTextString(m) }
func (*Entry) ProtoMessage() {}
func (*Entry) Descriptor() ([]byte, []int) {
return fileDescriptor_dd517c38176c13bf, []int{1}
}
func (m *Entry) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_Entry.Unmarshal(m, b)
}
func (m *Entry) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_Entry.Marshal(b, m, deterministic)
}
func (m *Entry) XXX_Merge(src proto.Message) {
xxx_messageInfo_Entry.Merge(m, src)
}
func (m *Entry) XXX_Size() int {
return xxx_messageInfo_Entry.Size(m)
}
func (m *Entry) XXX_DiscardUnknown() {
xxx_messageInfo_Entry.DiscardUnknown(m)
}
var xxx_messageInfo_Entry proto.InternalMessageInfo
func (m *Entry) GetId() string {
if m != nil {
return m.Id
}
return ""
}
func (m *Entry) GetDomain() string {
if m != nil {
return m.Domain
}
return ""
}
func (m *Entry) GetUrl() string {
if m != nil {
return m.Url
}
return ""
}
func (m *Entry) GetTitle() string {
if m != nil {
return m.Title
}
return ""
}
func (m *Entry) GetContent() string {
if m != nil {
return m.Content
}
return ""
}
func (m *Entry) GetDate() int64 {
if m != nil {
return m.Date
}
return 0
}
type NewRequest struct {
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
Address string `protobuf:"bytes,2,opt,name=address,proto3" json:"address,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *NewRequest) Reset() { *m = NewRequest{} }
func (m *NewRequest) String() string { return proto.CompactTextString(m) }
func (*NewRequest) ProtoMessage() {}
func (*NewRequest) Descriptor() ([]byte, []int) {
return fileDescriptor_dd517c38176c13bf, []int{2}
}
func (m *NewRequest) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_NewRequest.Unmarshal(m, b)
}
func (m *NewRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_NewRequest.Marshal(b, m, deterministic)
}
func (m *NewRequest) XXX_Merge(src proto.Message) {
xxx_messageInfo_NewRequest.Merge(m, src)
}
func (m *NewRequest) XXX_Size() int {
return xxx_messageInfo_NewRequest.Size(m)
}
func (m *NewRequest) XXX_DiscardUnknown() {
xxx_messageInfo_NewRequest.DiscardUnknown(m)
}
var xxx_messageInfo_NewRequest proto.InternalMessageInfo
func (m *NewRequest) GetName() string {
if m != nil {
return m.Name
}
return ""
}
func (m *NewRequest) GetAddress() string {
if m != nil {
return m.Address
}
return ""
}
type NewResponse struct {
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_unrecognized []byte `json:"-"`
XXX_sizecache int32 `json:"-"`
}
func (m *NewResponse) Reset() { *m = NewResponse{} }
func (m *NewResponse) String() string { return proto.CompactTextString(m) }
func (*NewResponse) ProtoMessage() {}
func (*NewResponse) Descriptor() ([]byte, []int) {
return fileDescriptor_dd517c38176c13bf, []int{3}
}
func (m *NewResponse) XXX_Unmarshal(b []byte) error {
return xxx_messageInfo_NewResponse.Unmarshal(m, b)
}
func (m *NewResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
return xxx_messageInfo_NewResponse.Marshal(b, m, deterministic)
}
func (m *NewResponse) XXX_Merge(src proto.Message) {
xxx_messageInfo_NewResponse.Merge(m, src)
}
func (m *NewResponse) XXX_Size() int {
return xxx_messageInfo_NewResponse.Size(m)
}
func (m *NewResponse) XXX_DiscardUnknown() {
xxx_messageInfo_NewResponse.DiscardUnknown(m)
}
var xxx_messageInfo_NewResponse proto.InternalMessageInfo
func init() {
proto.RegisterType((*Feed)(nil), "feeds.Feed")
proto.RegisterType((*Entry)(nil), "feeds.Entry")
proto.RegisterType((*NewRequest)(nil), "feeds.NewRequest")
proto.RegisterType((*NewResponse)(nil), "feeds.NewResponse")
}
func init() {
proto.RegisterFile("proto/feeds.proto", fileDescriptor_dd517c38176c13bf)
}
var fileDescriptor_dd517c38176c13bf = []byte{
// 235 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x94, 0x90, 0xb1, 0x4a, 0x04, 0x31,
0x10, 0x86, 0xdd, 0xdb, 0xcd, 0x8a, 0x73, 0x9c, 0x78, 0x83, 0x48, 0xb0, 0x92, 0xad, 0xac, 0x56,
0x50, 0x41, 0xd0, 0x4e, 0xd0, 0xd2, 0x62, 0x4b, 0xbb, 0xd5, 0x8c, 0xb0, 0x70, 0x97, 0x9c, 0xc9,
0x2c, 0xe2, 0x03, 0xf8, 0xde, 0x26, 0x93, 0x88, 0xb6, 0x76, 0xff, 0xf7, 0x85, 0x3f, 0x93, 0x0c,
0xac, 0x77, 0xde, 0xb1, 0xbb, 0x78, 0x23, 0x32, 0xa1, 0x97, 0x8c, 0x4a, 0xa0, 0xbb, 0x86, 0xe6,
0x31, 0x06, 0x44, 0x68, 0xec, 0xb8, 0x25, 0x5d, 0x9d, 0x55, 0xe7, 0x07, 0x83, 0x64, 0xd4, 0xb0,
0x3f, 0x1a, 0xe3, 0x29, 0x04, 0xbd, 0x10, 0xfd, 0x83, 0xdd, 0x57, 0x05, 0xea, 0xc1, 0xb2, 0xff,
0xc4, 0x43, 0x58, 0x4c, 0xa6, 0xb4, 0x62, 0xc2, 0x13, 0x68, 0x8d, 0xdb, 0x8e, 0x93, 0x2d, 0x95,
0x42, 0x78, 0x04, 0xf5, 0xec, 0x37, 0xba, 0x16, 0x99, 0x22, 0x1e, 0x83, 0xe2, 0x89, 0x37, 0xa4,
0x1b, 0x71, 0x19, 0xd2, 0xcc, 0x57, 0x67, 0x99, 0x2c, 0x6b, 0x95, 0x67, 0x16, 0x4c, 0x2f, 0x34,
0x23, 0x93, 0x6e, 0xa3, 0xae, 0x07, 0xc9, 0xdd, 0x2d, 0xc0, 0x13, 0x7d, 0x0c, 0xf4, 0x3e, 0x53,
0xe0, 0x7f, 0xfe, 0x61, 0x05, 0x4b, 0xe9, 0x86, 0x9d, 0xb3, 0x81, 0x2e, 0x6f, 0x40, 0xa5, 0x45,
0x04, 0xec, 0xa1, 0x8e, 0x1e, 0xd7, 0x7d, 0xde, 0xd6, 0xef, 0xfd, 0xa7, 0xf8, 0x57, 0xe5, 0x5a,
0xb7, 0x77, 0xbf, 0x7a, 0x5e, 0xca, 0x46, 0xef, 0xe4, 0xf0, 0xa5, 0x15, 0xb8, 0xfa, 0x0e, 0x00,
0x00, 0xff, 0xff, 0x04, 0x4b, 0x80, 0xda, 0x73, 0x01, 0x00, 0x00,
}

View File

@@ -0,0 +1,93 @@
// Code generated by protoc-gen-micro. DO NOT EDIT.
// source: proto/feeds.proto
package feeds
import (
fmt "fmt"
proto "github.com/golang/protobuf/proto"
math "math"
)
import (
context "context"
api "github.com/micro/micro/v3/service/api"
client "github.com/micro/micro/v3/service/client"
server "github.com/micro/micro/v3/service/server"
)
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
// Reference imports to suppress errors if they are not otherwise used.
var _ api.Endpoint
var _ context.Context
var _ client.Option
var _ server.Option
// Api Endpoints for Feeds service
func NewFeedsEndpoints() []*api.Endpoint {
return []*api.Endpoint{}
}
// Client API for Feeds service
type FeedsService interface {
New(ctx context.Context, in *NewRequest, opts ...client.CallOption) (*NewResponse, error)
}
type feedsService struct {
c client.Client
name string
}
func NewFeedsService(name string, c client.Client) FeedsService {
return &feedsService{
c: c,
name: name,
}
}
func (c *feedsService) New(ctx context.Context, in *NewRequest, opts ...client.CallOption) (*NewResponse, error) {
req := c.c.NewRequest(c.name, "Feeds.New", in)
out := new(NewResponse)
err := c.c.Call(ctx, req, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// Server API for Feeds service
type FeedsHandler interface {
New(context.Context, *NewRequest, *NewResponse) error
}
func RegisterFeedsHandler(s server.Server, hdlr FeedsHandler, opts ...server.HandlerOption) error {
type feeds interface {
New(ctx context.Context, in *NewRequest, out *NewResponse) error
}
type Feeds struct {
feeds
}
h := &feedsHandler{hdlr}
return s.Handle(s.NewHandler(&Feeds{h}, opts...))
}
type feedsHandler struct {
FeedsHandler
}
func (h *feedsHandler) New(ctx context.Context, in *NewRequest, out *NewResponse) error {
return h.FeedsHandler.New(ctx, in, out)
}

View File

@@ -0,0 +1,33 @@
syntax = "proto3";
package feeds;
option go_package = "proto;feeds";
service Feeds {
rpc New(NewRequest) returns (NewResponse) {}
}
message Feed {
// rss feed name
string name = 1;
// rss feed address
string address = 2;
}
message Entry {
string id = 1;
string domain = 2;
string url = 3;
string title = 4;
string content = 5;
int64 date = 6;
}
message NewRequest {
string name = 1;
string address = 2;
}
message NewResponse {
}