improve true remote timeline

This commit is contained in:
localhost_frssoft 2023-11-13 02:54:19 +03:00
parent eedd9d2c5c
commit 0f33427d5b
6 changed files with 160 additions and 23 deletions

View File

@ -256,6 +256,11 @@ type Attachment struct {
TextURL string `json:"text_url"` TextURL string `json:"text_url"`
Description string `json:"description"` Description string `json:"description"`
Meta AttachmentMeta `json:"meta"` Meta AttachmentMeta `json:"meta"`
//Misskey fields
Comment string `json:"comment"`
ThumbnailUrl string `json:"thumbnailUrl"`
Sensitive bool `json:"isSensitive"`
} }
// AttachmentMeta holds information for attachment metadata. // AttachmentMeta holds information for attachment metadata.

View File

@ -12,6 +12,7 @@ import (
"time" "time"
"encoding/json" "encoding/json"
"strconv" "strconv"
"strings"
) )
type StatusPleroma struct { type StatusPleroma struct {
@ -91,6 +92,28 @@ type Status struct {
RetweetedByID string `json:"retweeted_by_id"` RetweetedByID string `json:"retweeted_by_id"`
} }
type MisskeyStatus struct {
ID string `json:"id"`
User AccountMisskey `json:"user"`
CreatedAt CreatedAt `json:"createdAt"`
Visibility string `json:"visibility"`
CW string `json:"cw"`
URL string `json:"url"`
Text string `json:"text"`
Files []Attachment `json:"files"`
RenoteCount int64 `json:"renoteCount"`
RepliesCount int64 `json:"repliesCount"`
Reactions map[string]int `json:"reactions"`
}
type AccountMisskey struct {
ID string `json:"id"`
Name string `json:"name"`
Username string `json:"username"`
AvatarURL string `json:"avatarUrl"`
IsBot bool `json:"isBot"`
}
// Context hold information for mastodon context. // Context hold information for mastodon context.
type Context struct { type Context struct {
Ancestors []*Status `json:"ancestors"` Ancestors []*Status `json:"ancestors"`
@ -229,16 +252,15 @@ func (c *Client) GetTimelineHome(ctx context.Context, pg *Pagination) ([]*Status
return statuses, nil return statuses, nil
} }
type RemoteTimelineInstance struct {
http.Client
}
// TrueRemoteTimeline get public timeline from remote Mastodon API compatible instance directly // TrueRemoteTimeline get public timeline from remote Mastodon API compatible instance directly
func (c *Client) TrueRemoteTimeline(ctx context.Context, instance string, pg *Pagination) ([]*Status, error) { func (c *Client) TrueRemoteTimeline(ctx context.Context, instance string, instance_type string, pg *Pagination) ([]*Status, error) {
var httpclient RemoteTimelineInstance
var publicstatuses []*Status var publicstatuses []*Status
var instanceParams []string
instanceParams = strings.Split(instance, ":")[1:]
instance = strings.Split(instance, ":")[0]
params := url.Values{} params := url.Values{}
params.Set("local", "true") params.Set("local", "true")
if pg != nil { if pg != nil {
params = pg.setValues(params) params = pg.setValues(params)
} }
@ -246,32 +268,115 @@ func (c *Client) TrueRemoteTimeline(ctx context.Context, instance string, pg *Pa
perform := url.URL{ perform := url.URL{
Scheme: "https", Scheme: "https",
Host: instance, Host: instance,
Path: "api/v1/timelines/public",
RawQuery: params.Encode(), RawQuery: params.Encode(),
} }
withFiles := "false"
req, err := http.NewRequest(http.MethodGet, perform.String(), nil) withReplies := "false"
if err != nil { for _, instanceParam := range instanceParams {
return nil, err switch instanceParam {
case "withFiles":
withFiles = "true"
case "withReplies":
withReplies = "true"
default:
params.Set(instanceParam, "true")
}
} }
var method string
var ContentType string
var bytesAttach []byte
switch instance_type {
case "misskey":
perform.Path = "api/notes/local-timeline"
perform.RawQuery = ""
method = http.MethodPost
ContentType = "application/json"
bytesAttach = []byte(fmt.Sprintf(
`{"limit":20,"withRenotes":false, "withReplies": %s, "withFiles": %s}`,
withReplies, withFiles))
if pg != nil {
if pg.MaxID != "" {
bytesAttach = []byte(fmt.Sprintf(
`{"limit": %s,"withRenotes": false,"untilId":"%s", "withReplies": %s, "withFiles": %s}`,
strconv.Itoa(int(pg.Limit)), pg.MaxID, withReplies, withFiles))
}
}
default:
perform.Path = "api/v1/timelines/public"
method = http.MethodGet
ContentType = "application/x-www-form-urlencoded"
bytesAttach = []byte("")
}
req, err := http.NewRequest(method, perform.String(), bytes.NewBuffer(bytesAttach))
req = req.WithContext(ctx) req = req.WithContext(ctx)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", ContentType)
req.Header.Set("User-Agent", "Bloat") req.Header.Set("User-Agent", "Bloat")
resp, err := httpclient.Do(req) client := http.Client{}
resp, err := client.Do(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return nil, parseAPIError("bad request", resp) return nil, parseAPIError("Can't get remote timeline for " + instance + ", try select another type instance. Error" , resp)
} }
switch instance_type {
err = json.NewDecoder(resp.Body).Decode(&publicstatuses) case "misskey":
if err != nil { var misskeyData []MisskeyStatus
return nil, err err = json.NewDecoder(resp.Body).Decode(&misskeyData)
if err != nil {
return nil, err
}
for _, statusMisskey := range misskeyData {
var status Status
status.ID = statusMisskey.ID
status.Account.ID = statusMisskey.User.ID
status.Account.DisplayName = statusMisskey.User.Name
status.Account.Acct = statusMisskey.User.Username
status.Account.Username = statusMisskey.User.Username
status.Account.Avatar = statusMisskey.User.AvatarURL
status.CreatedAt = statusMisskey.CreatedAt
status.Visibility = statusMisskey.Visibility
status.Content = strings.Replace(statusMisskey.Text, "\n", "<br>", -1) + "<br><br>"
for reaction, count := range statusMisskey.Reactions { // woozyface
if reaction == "❤" {
status.FavouritesCount = int64(count)
continue
}
status.Content = status.Content + "[" + reaction + strconv.Itoa(count) + "]"
}
status.MediaAttachments = statusMisskey.Files
for idx, attach := range statusMisskey.Files {
status.MediaAttachments[idx].Type = strings.Split(attach.Type, "/")[0]
status.MediaAttachments[idx].Description = strings.Replace(attach.Comment, "\n", "<br>", -1)
status.MediaAttachments[idx].PreviewURL = attach.ThumbnailUrl
status.MediaAttachments[idx].RemoteURL = attach.URL
status.MediaAttachments[idx].TextURL = attach.URL
if status.Sensitive == false {
if attach.Sensitive { // mark status as NSFW if any attachment marked as NSFW
status.Sensitive = true
}
}
}
if statusMisskey.CW != "" {
status.Sensitive = true
status.SpoilerText = statusMisskey.CW
}
status.RepliesCount = statusMisskey.RepliesCount
status.ReblogsCount = statusMisskey.RenoteCount
status.Account.Bot = statusMisskey.User.IsBot
status.URL = "https://" + instance + "/notes/" + statusMisskey.ID
publicstatuses = append(publicstatuses, &status)
}
default:
err = json.NewDecoder(resp.Body).Decode(&publicstatuses)
if err != nil {
return nil, err
}
} }
return publicstatuses, nil return publicstatuses, nil
} }

View File

@ -59,6 +59,7 @@ type TimelineData struct {
Title string Title string
Type string Type string
Instance string Instance string
InstanceType string
Statuses []*mastodon.Status Statuses []*mastodon.Status
NextLink string NextLink string
PrevLink string PrevLink string

View File

@ -117,7 +117,7 @@ func (s *service) NavPage(c *client) (err error) {
} }
func (s *service) TimelinePage(c *client, tType, instance, listId, maxID, func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
minID string, tag string) (err error) { minID string, tag string, instance_type string) (err error) {
var nextLink, prevLink, title string var nextLink, prevLink, title string
var statuses []*mastodon.Status var statuses []*mastodon.Status
@ -159,13 +159,17 @@ func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
title = "Remote Timeline" title = "Remote Timeline"
case "tremote": case "tremote":
if len(instance) > 0 { if len(instance) > 0 {
statuses, err = c.TrueRemoteTimeline(c.ctx, instance, &pg) if instance_type == "" {
instance_type = "mastodon-compatible"
}
statuses, err = c.TrueRemoteTimeline(c.ctx, instance, instance_type, &pg)
if err != nil { if err != nil {
return err return err
} }
v := make(url.Values) v := make(url.Values)
v.Set("max_id", statuses[len(statuses)-1].ID) v.Set("max_id", statuses[len(statuses)-1].ID)
v.Set("instance", instance) v.Set("instance", instance)
v.Set("instance_type", instance_type)
nextLink = "/timeline/" + tType + "?" + v.Encode() nextLink = "/timeline/" + tType + "?" + v.Encode()
} }
title = "True Remote Timeline" title = "True Remote Timeline"
@ -223,6 +227,9 @@ func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
if len(instance) > 0 { if len(instance) > 0 {
v.Set("instance", instance) v.Set("instance", instance)
} }
if len(instance_type) > 0 {
v.Set("instance_type", instance_type)
}
if len(tag) > 0 { if len(tag) > 0 {
v.Set("tag", tag) v.Set("tag", tag)
} }
@ -235,9 +242,13 @@ func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
if len(minID) > 0 || (len(pg.MaxID) > 0 && len(statuses) == 20) { if len(minID) > 0 || (len(pg.MaxID) > 0 && len(statuses) == 20) {
v := make(url.Values) v := make(url.Values)
v.Set("max_id", pg.MaxID) v.Set("max_id", pg.MaxID)
v.Set("max_id", statuses[len(statuses)-1].ID)
if len(instance) > 0 { if len(instance) > 0 {
v.Set("instance", instance) v.Set("instance", instance)
} }
if len(instance_type) > 0 {
v.Set("instance_type", instance_type)
}
if len(tag) > 0 { if len(tag) > 0 {
v.Set("tag", tag) v.Set("tag", tag)
} }
@ -252,6 +263,7 @@ func (s *service) TimelinePage(c *client, tType, instance, listId, maxID,
Title: title, Title: title,
Type: tType, Type: tType,
Instance: instance, Instance: instance,
InstanceType: instance_type,
Statuses: statuses, Statuses: statuses,
NextLink: nextLink, NextLink: nextLink,
PrevLink: prevLink, PrevLink: prevLink,

View File

@ -139,7 +139,8 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
maxID := q.Get("max_id") maxID := q.Get("max_id")
minID := q.Get("min_id") minID := q.Get("min_id")
tag := q.Get("tag") tag := q.Get("tag")
return s.TimelinePage(c, tType, instance, list, maxID, minID, tag) instance_type := q.Get("instance_type")
return s.TimelinePage(c, tType, instance, list, maxID, minID, tag, instance_type)
}, SESSION, HTML) }, SESSION, HTML)
defaultTimelinePage := handle(func(c *client) error { defaultTimelinePage := handle(func(c *client) error {

View File

@ -14,10 +14,23 @@
<button type="submit"> Submit </button> <button type="submit"> Submit </button>
</form> </form>
{{if eq .Instance ""}} {{if eq .Instance ""}}
<a href="/timeline/tremote"> True remote timeline viewer </a>
{{else}} {{else}}
<a href="/timeline/tremote?instance={{.Instance}}"> Look via True remote timeline viewer (works with mastodon API compatible instances) </a>
{{end}} {{end}}
{{end}} {{end}}
{{if eq .Type "tremote"}}
<form class="search-form" action="/timeline/tremote" method="GET">
<span class="post-form-field">
<label for="instance"> Instance </label>
<input id="instance" name="instance" value="{{.Instance}}" placeholder="example.com:optional_param">
<select id="instance_type" name="instance_type" title="Select instance type">
<option value="mastodon-compatible" default>Mastodon compatible</option>
<option value="misskey"{{if eq .InstanceType "misskey"}}selected{{end}}>Misskey</option>
</select>
</span>
<button type="submit"> Submit </button>
</form>
{{end}}
{{if eq .Type "tag"}} {{if eq .Type "tag"}}
<form class="search-form" action="/timeline/tag" method="GET"> <form class="search-form" action="/timeline/tag" method="GET">
<span class="post-form-field"> <span class="post-form-field">