mirror of
https://github.com/mrusme/neonmodem.git
synced 2024-12-04 14:46:37 -05:00
Implemented Lobsters system
This commit is contained in:
parent
140633314f
commit
c7a6351e5c
@ -1,6 +1,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mrusme/gobbs/config"
|
||||
@ -47,7 +48,7 @@ func connectBase() *cobra.Command {
|
||||
LOG.Panicln(err)
|
||||
}
|
||||
|
||||
LOG.Infoln("Successfully added new connection!")
|
||||
fmt.Println("Successfully added new connection!")
|
||||
os.Exit(0)
|
||||
},
|
||||
}
|
||||
@ -58,7 +59,7 @@ func connectBase() *cobra.Command {
|
||||
&sysType,
|
||||
"type",
|
||||
"",
|
||||
"Type of system to connect to (discourse, lemmy, hackernews)",
|
||||
"Type of system to connect to (discourse, lemmy, lobsers, hackernews)",
|
||||
)
|
||||
cmd.MarkFlagRequired("type")
|
||||
|
||||
|
181
system/lobsters/api/client.go
Normal file
181
system/lobsters/api/client.go
Normal file
@ -0,0 +1,181 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/go-retryablehttp"
|
||||
)
|
||||
|
||||
type RequestError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (re *RequestError) Error() string {
|
||||
return re.Err.Error()
|
||||
}
|
||||
|
||||
type Logger interface {
|
||||
Debug(args ...interface{})
|
||||
Debugf(format string, args ...interface{})
|
||||
|
||||
Info(args ...interface{})
|
||||
Infof(format string, args ...interface{})
|
||||
|
||||
Warn(args ...interface{})
|
||||
Warnf(format string, args ...interface{})
|
||||
|
||||
Error(args ...interface{})
|
||||
Errorf(format string, args ...interface{})
|
||||
|
||||
Fatal(args ...interface{})
|
||||
Fatalf(format string, args ...interface{})
|
||||
}
|
||||
|
||||
type StdLogger struct {
|
||||
L Logger
|
||||
}
|
||||
|
||||
func NewStdLogger(l Logger) retryablehttp.Logger {
|
||||
return &StdLogger{
|
||||
L: l,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *StdLogger) Printf(message string, v ...interface{}) {
|
||||
l.L.Debug(message, v)
|
||||
}
|
||||
|
||||
type ClientConfig struct {
|
||||
Endpoint string
|
||||
Credentials map[string]string
|
||||
HTTPClient *http.Client
|
||||
Logger Logger
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
httpClient *retryablehttp.Client
|
||||
endpoint *url.URL
|
||||
credentials map[string]string
|
||||
logger Logger
|
||||
|
||||
Stories StoriesService
|
||||
Tags TagsService
|
||||
}
|
||||
|
||||
func NewDefaultClientConfig(
|
||||
endpoint string,
|
||||
credentials map[string]string,
|
||||
logger Logger,
|
||||
) ClientConfig {
|
||||
return ClientConfig{
|
||||
Endpoint: endpoint,
|
||||
Credentials: credentials,
|
||||
HTTPClient: http.DefaultClient,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func NewClient(cc *ClientConfig) *Client {
|
||||
c := new(Client)
|
||||
c.logger = cc.Logger
|
||||
c.httpClient = retryablehttp.NewClient()
|
||||
c.httpClient.RetryMax = 3
|
||||
if c.logger != nil {
|
||||
c.httpClient.Logger = NewStdLogger(c.logger)
|
||||
}
|
||||
c.httpClient.HTTPClient = cc.HTTPClient
|
||||
c.endpoint, _ = url.Parse(cc.Endpoint)
|
||||
c.credentials = cc.Credentials
|
||||
|
||||
c.Stories = &StoryServiceHandler{client: c}
|
||||
c.Tags = &TagServiceHandler{client: c}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *Client) NewRequest(
|
||||
ctx context.Context,
|
||||
method string,
|
||||
location string,
|
||||
body interface{},
|
||||
) (*http.Request, error) {
|
||||
var parsedURL *url.URL
|
||||
var req *http.Request
|
||||
var err error
|
||||
|
||||
if parsedURL, err = c.endpoint.Parse(location); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer := new(bytes.Buffer)
|
||||
if body != nil {
|
||||
if err = json.NewEncoder(buffer).Encode(body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Debug(buffer.String())
|
||||
|
||||
if req, err = http.NewRequest(
|
||||
method,
|
||||
parsedURL.String(),
|
||||
buffer,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add("User-Agent", "gobbs")
|
||||
req.Header.Add("Accept", "application/json")
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (c *Client) Do(
|
||||
ctx context.Context,
|
||||
req *http.Request,
|
||||
content interface{},
|
||||
) error {
|
||||
var rreq *retryablehttp.Request
|
||||
var res *http.Response
|
||||
var body []byte
|
||||
var err error
|
||||
|
||||
if rreq, err = retryablehttp.FromRequest(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rreq = rreq.WithContext(ctx)
|
||||
if res, err = c.httpClient.Do(rreq); err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if body, err = ioutil.ReadAll(res.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if content != nil {
|
||||
if err = json.Unmarshal(body, content); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.logger.Debug(res)
|
||||
c.logger.Debug(string(body))
|
||||
|
||||
if res.StatusCode < http.StatusOK ||
|
||||
res.StatusCode > http.StatusNoContent {
|
||||
return &RequestError{
|
||||
Err: errors.New("Non-2xx status code"),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
115
system/lobsters/api/stories.go
Normal file
115
system/lobsters/api/stories.go
Normal file
@ -0,0 +1,115 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const StoriesBaseURL = "/s"
|
||||
|
||||
type UserModel struct {
|
||||
Username string `json:"username"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
About string `json:"about"`
|
||||
IsModerator bool `json:"is_moderator"`
|
||||
Karma int `json:"karma"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
InvitedByUser string `json:"invited_by_user"`
|
||||
GithubUsername string `json:"github_username"`
|
||||
TwitterUsername string `json:"twitter_username"`
|
||||
}
|
||||
|
||||
type StoryModel struct {
|
||||
ShortID string `json:"short_id"`
|
||||
ShortIDURL string `json:"short_id_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
Score int `json:"score"`
|
||||
Flags int `json:"flags"`
|
||||
CommentCount int `json:"comment_count"`
|
||||
Description string `json:"description"`
|
||||
DescriptionPlain string `json:"description_plain"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
CategoryID int `json:"category_id"`
|
||||
SubmitterUser UserModel `json:"submitter_user"`
|
||||
Tags []string `json:"tags"`
|
||||
Comments []struct {
|
||||
ShortID string `json:"short_id"`
|
||||
ShortIDURL string `json:"short_id_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
IsDeleted bool `json:"is_deleted"`
|
||||
IsModerated bool `json:"is_moderated"`
|
||||
Score int `json:"score"`
|
||||
Flags int `json:"flags"`
|
||||
ParentComment string `json:"parent_comment"`
|
||||
Comment string `json:"comment"`
|
||||
CommentPlain string `json:"comment_plain"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
URL string `json:"url"`
|
||||
IndentLevel int `json:"indent_level"`
|
||||
CommentingUser UserModel `json:"commenting_user"`
|
||||
} `json:"comments"`
|
||||
}
|
||||
|
||||
type StoriesService interface {
|
||||
Show(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (*StoryModel, error)
|
||||
List(
|
||||
ctx context.Context,
|
||||
tag string,
|
||||
) (*[]StoryModel, error)
|
||||
}
|
||||
|
||||
type StoryServiceHandler struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Show
|
||||
func (a *StoryServiceHandler) Show(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
) (*StoryModel, error) {
|
||||
uri := StoriesBaseURL + "/" + id + ".json"
|
||||
|
||||
req, err := a.client.NewRequest(ctx, http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := new(StoryModel)
|
||||
if err = a.client.Do(ctx, req, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// List
|
||||
func (a *StoryServiceHandler) List(
|
||||
ctx context.Context,
|
||||
tag string,
|
||||
) (*[]StoryModel, error) {
|
||||
var uri string
|
||||
if tag == "" {
|
||||
uri = "/newest.json"
|
||||
} else {
|
||||
uri = "/t/" + tag + ".json"
|
||||
}
|
||||
|
||||
req, err := a.client.NewRequest(ctx, http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := new([]StoryModel)
|
||||
if err = a.client.Do(ctx, req, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
49
system/lobsters/api/tags.go
Normal file
49
system/lobsters/api/tags.go
Normal file
@ -0,0 +1,49 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const TagsBaseURL = "/tags"
|
||||
|
||||
type TagModel struct {
|
||||
ID int `json:"id"`
|
||||
Tag string `json:"tag"`
|
||||
Description string `json:"description"`
|
||||
Privileged bool `json:"privileged"`
|
||||
IsMedia bool `json:"is_media"`
|
||||
Active bool `json:"active"`
|
||||
HotnessMod float32 `json:"hotness_mod"`
|
||||
PermitByNewUsers bool `json:"permit_by_new_users"`
|
||||
CategoryID int `json:"category_id"`
|
||||
}
|
||||
|
||||
type TagsService interface {
|
||||
List(
|
||||
ctx context.Context,
|
||||
) (*[]TagModel, error)
|
||||
}
|
||||
|
||||
type TagServiceHandler struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// List
|
||||
func (a *TagServiceHandler) List(
|
||||
ctx context.Context,
|
||||
) (*[]TagModel, error) {
|
||||
uri := TagsBaseURL + ".json"
|
||||
|
||||
req, err := a.client.NewRequest(ctx, http.MethodGet, uri, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := new([]TagModel)
|
||||
if err = a.client.Do(ctx, req, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
21
system/lobsters/connect.go
Normal file
21
system/lobsters/connect.go
Normal file
@ -0,0 +1,21 @@
|
||||
package lobsters
|
||||
|
||||
type UserAPIKey struct {
|
||||
Key string `json:"key"`
|
||||
Nonce string `json:"nonce"`
|
||||
Push bool `json:"push"`
|
||||
API int `json:"api"`
|
||||
}
|
||||
|
||||
func (sys *System) Connect(sysURL string) error {
|
||||
// Credentials
|
||||
credentials := make(map[string]string)
|
||||
|
||||
if sys.config == nil {
|
||||
sys.config = make(map[string]interface{})
|
||||
}
|
||||
sys.config["url"] = sysURL
|
||||
sys.config["credentials"] = credentials
|
||||
|
||||
return nil
|
||||
}
|
239
system/lobsters/lobsters.go
Normal file
239
system/lobsters/lobsters.go
Normal file
@ -0,0 +1,239 @@
|
||||
package lobsters
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
md "github.com/JohannesKaufmann/html-to-markdown"
|
||||
"github.com/araddon/dateparse"
|
||||
"github.com/mrusme/gobbs/models/author"
|
||||
"github.com/mrusme/gobbs/models/forum"
|
||||
"github.com/mrusme/gobbs/models/post"
|
||||
"github.com/mrusme/gobbs/models/reply"
|
||||
"github.com/mrusme/gobbs/system/adapter"
|
||||
"github.com/mrusme/gobbs/system/lobsters/api"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type System struct {
|
||||
ID int
|
||||
config map[string]interface{}
|
||||
logger *zap.SugaredLogger
|
||||
client *api.Client
|
||||
}
|
||||
|
||||
func (sys *System) GetID() int {
|
||||
return sys.ID
|
||||
}
|
||||
|
||||
func (sys *System) SetID(id int) {
|
||||
sys.ID = id
|
||||
}
|
||||
|
||||
func (sys *System) GetConfig() map[string]interface{} {
|
||||
return sys.config
|
||||
}
|
||||
|
||||
func (sys *System) SetConfig(cfg *map[string]interface{}) {
|
||||
sys.config = *cfg
|
||||
}
|
||||
|
||||
func (sys *System) SetLogger(logger *zap.SugaredLogger) {
|
||||
sys.logger = logger
|
||||
}
|
||||
|
||||
func (sys *System) GetCapabilities() adapter.Capabilities {
|
||||
var caps []adapter.Capability
|
||||
|
||||
caps = append(caps,
|
||||
adapter.Capability{
|
||||
ID: "list:forums",
|
||||
Name: "List Forums",
|
||||
},
|
||||
adapter.Capability{
|
||||
ID: "list:posts",
|
||||
Name: "List Posts",
|
||||
},
|
||||
// adapter.Capability{
|
||||
// ID: "create:post",
|
||||
// Name: "Create Post",
|
||||
// },
|
||||
adapter.Capability{
|
||||
ID: "list:replies",
|
||||
Name: "List Replies",
|
||||
},
|
||||
// adapter.Capability{
|
||||
// ID: "create:reply",
|
||||
// Name: "Create Reply",
|
||||
// },
|
||||
)
|
||||
|
||||
return caps
|
||||
}
|
||||
|
||||
func (sys *System) FilterValue() string {
|
||||
return fmt.Sprintf(
|
||||
"Lobsters %s",
|
||||
sys.config["url"],
|
||||
)
|
||||
}
|
||||
|
||||
func (sys *System) Title() string {
|
||||
sysUrl := sys.config["url"].(string)
|
||||
u, err := url.Parse(sysUrl)
|
||||
if err != nil {
|
||||
return sysUrl
|
||||
}
|
||||
|
||||
return u.Hostname()
|
||||
}
|
||||
|
||||
func (sys *System) Description() string {
|
||||
return fmt.Sprintf(
|
||||
"Lobsters",
|
||||
)
|
||||
}
|
||||
|
||||
func (sys *System) Load() error {
|
||||
url := sys.config["url"]
|
||||
if url == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
credentials := make(map[string]string)
|
||||
|
||||
sys.client = api.NewClient(&api.ClientConfig{
|
||||
Endpoint: url.(string),
|
||||
Credentials: credentials,
|
||||
HTTPClient: http.DefaultClient,
|
||||
Logger: sys.logger,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sys *System) ListForums() ([]forum.Forum, error) {
|
||||
var models []forum.Forum
|
||||
|
||||
tags, err := sys.client.Tags.List(context.Background())
|
||||
if err != nil {
|
||||
return []forum.Forum{}, err
|
||||
}
|
||||
|
||||
for _, tag := range *tags {
|
||||
models = append(models, forum.Forum{
|
||||
ID: tag.Tag,
|
||||
Name: tag.Description,
|
||||
|
||||
SysIDX: sys.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func (sys *System) ListPosts(forumID string) ([]post.Post, error) {
|
||||
var err error
|
||||
|
||||
items, err := sys.client.Stories.List(context.Background(), forumID)
|
||||
if err != nil {
|
||||
return []post.Post{}, err
|
||||
}
|
||||
|
||||
var models []post.Post
|
||||
for _, i := range *items {
|
||||
createdAt, err := dateparse.ParseAny(i.CreatedAt)
|
||||
if err != nil {
|
||||
createdAt = time.Now() // TODO: Errrr
|
||||
}
|
||||
|
||||
models = append(models, post.Post{
|
||||
ID: i.ShortID,
|
||||
|
||||
Subject: i.Title,
|
||||
|
||||
Type: "url",
|
||||
|
||||
Pinned: false,
|
||||
Closed: false,
|
||||
|
||||
CreatedAt: createdAt,
|
||||
LastCommentedAt: createdAt, // TODO
|
||||
|
||||
Author: author.Author{
|
||||
ID: i.SubmitterUser.Username,
|
||||
Name: i.SubmitterUser.Username,
|
||||
},
|
||||
|
||||
Forum: forum.Forum{
|
||||
ID: i.Tags[0],
|
||||
Name: i.Tags[0], // TODO: Tag description
|
||||
|
||||
SysIDX: sys.ID,
|
||||
},
|
||||
|
||||
SysIDX: sys.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func (sys *System) LoadPost(p *post.Post) error {
|
||||
item, err := sys.client.Stories.Show(context.Background(), p.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
converter := md.NewConverter("", true, nil)
|
||||
|
||||
for idx, i := range item.Comments {
|
||||
cookedMd, err := converter.ConvertString(i.Comment)
|
||||
if err != nil {
|
||||
cookedMd = i.CommentPlain
|
||||
}
|
||||
|
||||
if idx == 0 {
|
||||
p.Body = cookedMd
|
||||
continue
|
||||
}
|
||||
|
||||
createdAt, err := dateparse.ParseAny(i.CreatedAt)
|
||||
if err != nil {
|
||||
createdAt = time.Now() // TODO: Errrrrr
|
||||
}
|
||||
|
||||
inReplyTo := i.ParentComment
|
||||
if inReplyTo == "" {
|
||||
inReplyTo = p.ID
|
||||
}
|
||||
p.Replies = append(p.Replies, reply.Reply{
|
||||
ID: i.ShortID,
|
||||
InReplyTo: inReplyTo,
|
||||
|
||||
Body: cookedMd,
|
||||
|
||||
CreatedAt: createdAt,
|
||||
|
||||
Author: author.Author{
|
||||
ID: i.CommentingUser.Username,
|
||||
Name: i.CommentingUser.Username,
|
||||
},
|
||||
|
||||
SysIDX: sys.ID,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sys *System) CreatePost(p *post.Post) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sys *System) CreateReply(r *reply.Reply) error {
|
||||
return nil
|
||||
}
|
@ -11,6 +11,7 @@ import (
|
||||
"github.com/mrusme/gobbs/system/discourse"
|
||||
"github.com/mrusme/gobbs/system/hackernews"
|
||||
"github.com/mrusme/gobbs/system/lemmy"
|
||||
"github.com/mrusme/gobbs/system/lobsters"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@ -48,6 +49,8 @@ func New(
|
||||
sys = new(discourse.System)
|
||||
case "lemmy":
|
||||
sys = new(lemmy.System)
|
||||
case "lobsters":
|
||||
sys = new(lobsters.System)
|
||||
case "hackernews":
|
||||
sys = new(hackernews.System)
|
||||
case "all":
|
||||
|
Loading…
Reference in New Issue
Block a user