1
0
mirror of https://github.com/mrusme/neonmodem.git synced 2024-06-16 06:25:23 +00:00

Implemented Lobsters system

This commit is contained in:
マリウス 2023-01-05 14:57:01 -05:00
parent 140633314f
commit c7a6351e5c
No known key found for this signature in database
GPG Key ID: 272ED814BF63261F
7 changed files with 611 additions and 2 deletions

View File

@ -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")

View 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
}

View 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
}

View 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
}

View 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
View 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
}

View File

@ -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":