Compare commits
No commits in common. "f4881e72675e87a9eae716436c3ac18a788d596d" and "81bdc7c705d5d21f62927167d5b2c8e4932c9570" have entirely different histories.
f4881e7267
...
81bdc7c705
|
@ -38,4 +38,5 @@ post_formats=PlainText:text/plain,HTML:text/html,Markdown:text/markdown,BBCode:t
|
||||||
# single_instance=pl.mydomain.com
|
# single_instance=pl.mydomain.com
|
||||||
|
|
||||||
# Path to custom CSS. Value can be a file path relative to the static directory.
|
# Path to custom CSS. Value can be a file path relative to the static directory.
|
||||||
|
# or a URL starting with either "http://" or "https://".
|
||||||
# custom_css=custom.css
|
# custom_css=custom.css
|
||||||
|
|
9
main.go
9
main.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"bloat/config"
|
"bloat/config"
|
||||||
"bloat/renderer"
|
"bloat/renderer"
|
||||||
|
@ -46,8 +47,14 @@ func main() {
|
||||||
errExit(err)
|
errExit(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customCSS := config.CustomCSS
|
||||||
|
if len(customCSS) > 0 && !strings.HasPrefix(customCSS, "http://") &&
|
||||||
|
!strings.HasPrefix(customCSS, "https://") {
|
||||||
|
customCSS = "/static/" + customCSS
|
||||||
|
}
|
||||||
|
|
||||||
s := service.NewService(config.ClientName, config.ClientScope,
|
s := service.NewService(config.ClientName, config.ClientScope,
|
||||||
config.ClientWebsite, config.CustomCSS, config.SingleInstance,
|
config.ClientWebsite, customCSS, config.SingleInstance,
|
||||||
config.PostFormats, renderer)
|
config.PostFormats, renderer)
|
||||||
handler := service.NewHandler(s, *verbose, config.StaticDirectory)
|
handler := service.NewHandler(s, *verbose, config.StaticDirectory)
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,18 @@
|
||||||
package mastodon
|
package mastodon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tomnomnom/linkheader"
|
"github.com/tomnomnom/linkheader"
|
||||||
|
@ -57,6 +61,83 @@ func (c *Client) doAPI(ctx context.Context, method string, uri string, params in
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
} else if file, ok := params.(string); ok {
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
mw := multipart.NewWriter(&buf)
|
||||||
|
part, err := mw.CreateFormFile("file", filepath.Base(file))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(part, f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = mw.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest(method, u.String(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ct = mw.FormDataContentType()
|
||||||
|
} else if file, ok := params.(*multipart.FileHeader); ok {
|
||||||
|
f, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
mw := multipart.NewWriter(&buf)
|
||||||
|
fname := filepath.Base(file.Filename)
|
||||||
|
err = mw.WriteField("description", fname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
part, err := mw.CreateFormFile("file", fname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(part, f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = mw.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest(method, u.String(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ct = mw.FormDataContentType()
|
||||||
|
} else if reader, ok := params.(io.Reader); ok {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
mw := multipart.NewWriter(&buf)
|
||||||
|
part, err := mw.CreateFormFile("file", "upload")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = io.Copy(part, reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = mw.Close()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err = http.NewRequest(method, u.String(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ct = mw.FormDataContentType()
|
||||||
} else if mr, ok := params.(*multipartRequest); ok {
|
} else if mr, ok := params.(*multipartRequest); ok {
|
||||||
req, err = http.NewRequest(method, u.String(), mr.Data)
|
req, err = http.NewRequest(method, u.String(), mr.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -138,16 +219,6 @@ func (c *Client) AuthenticateToken(ctx context.Context, authCode, redirectURI st
|
||||||
return c.authenticate(ctx, params)
|
return c.authenticate(ctx, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) RevokeToken(ctx context.Context) error {
|
|
||||||
params := url.Values{
|
|
||||||
"client_id": {c.config.ClientID},
|
|
||||||
"client_secret": {c.config.ClientSecret},
|
|
||||||
"token": {c.GetAccessToken(ctx)},
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.doAPI(ctx, http.MethodPost, "/oauth/revoke", params, nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Client) authenticate(ctx context.Context, params url.Values) error {
|
func (c *Client) authenticate(ctx context.Context, params url.Values) error {
|
||||||
u, err := url.Parse(c.config.Server)
|
u, err := url.Parse(c.config.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
package mastodon
|
package mastodon
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path/filepath"
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -295,35 +293,30 @@ func (c *Client) Search(ctx context.Context, q string, qType string, limit int,
|
||||||
return &results, nil
|
return &results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) {
|
// UploadMedia upload a media attachment from a file.
|
||||||
f, err := fh.Open()
|
func (c *Client) UploadMedia(ctx context.Context, file string) (*Attachment, error) {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
mw := multipart.NewWriter(&buf)
|
|
||||||
fname := filepath.Base(fh.Filename)
|
|
||||||
err = mw.WriteField("description", fname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
part, err := mw.CreateFormFile("file", fname)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
_, err = io.Copy(part, f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = mw.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
params := &multipartRequest{Data: &buf, ContentType: mw.FormDataContentType()}
|
|
||||||
var attachment Attachment
|
var attachment Attachment
|
||||||
err = c.doAPI(ctx, http.MethodPost, "/api/v1/media", params, &attachment, nil)
|
err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", file, &attachment, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &attachment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadMediaFromReader uploads a media attachment from a io.Reader.
|
||||||
|
func (c *Client) UploadMediaFromReader(ctx context.Context, reader io.Reader) (*Attachment, error) {
|
||||||
|
var attachment Attachment
|
||||||
|
err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", reader, &attachment, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &attachment, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadMediaFromReader uploads a media attachment from a io.Reader.
|
||||||
|
func (c *Client) UploadMediaFromMultipartFileHeader(ctx context.Context, fh *multipart.FileHeader) (*Attachment, error) {
|
||||||
|
var attachment Attachment
|
||||||
|
err := c.doAPI(ctx, http.MethodPost, "/api/v1/media", fh, &attachment, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package model
|
package model
|
||||||
|
|
||||||
type Session struct {
|
type Session struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
UserID string `json:"uid,omitempty"`
|
UserID string `json:"uid,omitempty"`
|
||||||
Instance string `json:"ins,omitempty"`
|
Instance string `json:"ins,omitempty"`
|
||||||
ClientID string `json:"cid,omitempty"`
|
ClientID string `json:"cid,omitempty"`
|
||||||
|
@ -27,7 +28,6 @@ type Settings struct {
|
||||||
AntiDopamineMode bool `json:"adm,omitempty"`
|
AntiDopamineMode bool `json:"adm,omitempty"`
|
||||||
HideUnsupportedNotifs bool `json:"hun,omitempty"`
|
HideUnsupportedNotifs bool `json:"hun,omitempty"`
|
||||||
CSS string `json:"css,omitempty"`
|
CSS string `json:"css,omitempty"`
|
||||||
CSSHash string `json:"cssh,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSettings() *Settings {
|
func NewSettings() *Settings {
|
||||||
|
@ -44,6 +44,5 @@ func NewSettings() *Settings {
|
||||||
AntiDopamineMode: false,
|
AntiDopamineMode: false,
|
||||||
HideUnsupportedNotifs: false,
|
HideUnsupportedNotifs: false,
|
||||||
CSS: "",
|
CSS: "",
|
||||||
CSSHash: "",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,8 +34,6 @@ func (c *client) setSession(sess *model.Session) error {
|
||||||
}
|
}
|
||||||
http.SetCookie(c.w, &http.Cookie{
|
http.SetCookie(c.w, &http.Cookie{
|
||||||
Name: "session",
|
Name: "session",
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
Value: sb.String(),
|
Value: sb.String(),
|
||||||
Expires: time.Now().Add(365 * 24 * time.Hour),
|
Expires: time.Now().Add(365 * 24 * time.Hour),
|
||||||
})
|
})
|
||||||
|
@ -55,7 +53,6 @@ func (c *client) getSession() (sess *model.Session, err error) {
|
||||||
func (c *client) unsetSession() {
|
func (c *client) unsetSession() {
|
||||||
http.SetCookie(c.w, &http.Cookie{
|
http.SetCookie(c.w, &http.Cookie{
|
||||||
Name: "session",
|
Name: "session",
|
||||||
Path: "/",
|
|
||||||
Value: "",
|
Value: "",
|
||||||
Expires: time.Now(),
|
Expires: time.Now(),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
|
@ -842,6 +840,10 @@ func (s *service) NewSession(c *client, instance string) (rurl string, sess *mod
|
||||||
instanceURL = "https://" + instance
|
instanceURL = "https://" + instance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sid, err := util.NewSessionID()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
csrf, err := util.NewCSRFToken()
|
csrf, err := util.NewCSRFToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@ -857,14 +859,28 @@ func (s *service) NewSession(c *client, instance string) (rurl string, sess *mod
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rurl = app.AuthURI
|
|
||||||
sess = &model.Session{
|
sess = &model.Session{
|
||||||
|
ID: sid,
|
||||||
Instance: instance,
|
Instance: instance,
|
||||||
ClientID: app.ClientID,
|
ClientID: app.ClientID,
|
||||||
ClientSecret: app.ClientSecret,
|
ClientSecret: app.ClientSecret,
|
||||||
CSRFToken: csrf,
|
CSRFToken: csrf,
|
||||||
Settings: *model.NewSettings(),
|
Settings: *model.NewSettings(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
u, err := url.Parse("/oauth/authorize")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
q := make(url.Values)
|
||||||
|
q.Set("scope", "read write follow")
|
||||||
|
q.Set("client_id", app.ClientID)
|
||||||
|
q.Set("response_type", "code")
|
||||||
|
q.Set("redirect_uri", s.cwebsite+"/oauth_callback")
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
rurl = instanceURL + u.String()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -886,10 +902,6 @@ func (s *service) Signin(c *client, code string) (err error) {
|
||||||
return c.setSession(c.s)
|
return c.setSession(c.s)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *service) Signout(c *client) (err error) {
|
|
||||||
return c.RevokeToken(c.ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *service) Post(c *client, content string, replyToID string,
|
func (s *service) Post(c *client, content string, replyToID string,
|
||||||
format string, visibility string, isNSFW bool,
|
format string, visibility string, isNSFW bool,
|
||||||
files []*multipart.FileHeader) (id string, err error) {
|
files []*multipart.FileHeader) (id string, err error) {
|
||||||
|
@ -1016,19 +1028,9 @@ func (s *service) SaveSettings(c *client, settings *model.Settings) (err error)
|
||||||
default:
|
default:
|
||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
if len(settings.CSS) > 0 {
|
|
||||||
if len(settings.CSS) > 1<<20 {
|
if len(settings.CSS) > 1<<20 {
|
||||||
return errInvalidArgument
|
return errInvalidArgument
|
||||||
}
|
}
|
||||||
// For some reason, browsers convert CRLF to LF before calculating
|
|
||||||
// the hash of the inline resources.
|
|
||||||
settings.CSS = strings.Replace(settings.CSS, "\x0d\x0a", "\x0a", -1)
|
|
||||||
|
|
||||||
h := sha256.Sum256([]byte(settings.CSS))
|
|
||||||
settings.CSSHash = base64.StdEncoding.EncodeToString(h[:])
|
|
||||||
} else {
|
|
||||||
settings.CSSHash = ""
|
|
||||||
}
|
|
||||||
c.s.Settings = *settings
|
c.s.Settings = *settings
|
||||||
return c.setSession(c.s)
|
return c.setSession(c.s)
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,15 +26,6 @@ const (
|
||||||
CSRF
|
CSRF
|
||||||
)
|
)
|
||||||
|
|
||||||
const csp = "default-src 'none';" +
|
|
||||||
" img-src *;" +
|
|
||||||
" media-src *;" +
|
|
||||||
" font-src *;" +
|
|
||||||
" child-src *;" +
|
|
||||||
" connect-src 'self';" +
|
|
||||||
" script-src 'self';" +
|
|
||||||
" style-src 'self'"
|
|
||||||
|
|
||||||
func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
|
|
||||||
|
@ -67,14 +58,14 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
}(time.Now())
|
}(time.Now())
|
||||||
}
|
}
|
||||||
|
|
||||||
h := c.w.Header()
|
var ct string
|
||||||
switch rt {
|
switch rt {
|
||||||
case HTML:
|
case HTML:
|
||||||
h.Set("Content-Type", "text/html; charset=utf-8")
|
ct = "text/html; charset=utf-8"
|
||||||
h.Set("Content-Security-Policy", csp)
|
|
||||||
case JSON:
|
case JSON:
|
||||||
h.Set("Content-Type", "application/json")
|
ct = "application/json"
|
||||||
}
|
}
|
||||||
|
c.w.Header().Add("Content-Type", ct)
|
||||||
|
|
||||||
err = c.authenticate(at, s.instance)
|
err = c.authenticate(at, s.instance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -82,13 +73,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override the CSP header to allow custom CSS
|
|
||||||
if rt == HTML && len(c.s.Settings.CSS) > 0 &&
|
|
||||||
len(c.s.Settings.CSSHash) > 0 {
|
|
||||||
v := fmt.Sprintf("%s 'sha256-%s'", csp, c.s.Settings.CSSHash)
|
|
||||||
h.Set("Content-Security-Policy", v)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = f(c)
|
err = f(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writeError(c, err, rt, req.Method == http.MethodGet)
|
writeError(c, err, rt, req.Method == http.MethodGet)
|
||||||
|
@ -692,10 +676,6 @@ func NewHandler(s *service, verbose bool, staticDir string) http.Handler {
|
||||||
}, CSRF, HTML)
|
}, CSRF, HTML)
|
||||||
|
|
||||||
signout := handle(func(c *client) error {
|
signout := handle(func(c *client) error {
|
||||||
err := s.Signout(c)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
c.unsetSession()
|
c.unsetSession()
|
||||||
c.redirect("/")
|
c.redirect("/")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -325,7 +325,7 @@ document.addEventListener("DOMContentLoaded", function() {
|
||||||
links[j].target = "_blank";
|
links[j].target = "_blank";
|
||||||
}
|
}
|
||||||
|
|
||||||
var links = document.querySelectorAll(".status-media-container .img-link, .user-profile-img-container .img-link");
|
var links = document.querySelectorAll(".status-media-container .img-link");
|
||||||
for (var j = 0; j < links.length; j++) {
|
for (var j = 0; j < links.length; j++) {
|
||||||
handleImgPreview(links[j]);
|
handleImgPreview(links[j]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
<title> {{if gt .Count 0}}({{.Count}}){{end}} {{.Title}} </title>
|
<title> {{if gt .Count 0}}({{.Count}}){{end}} {{.Title}} </title>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
{{if .CustomCSS}}
|
{{if .CustomCSS}}
|
||||||
<link rel="stylesheet" href="/static/{{.CustomCSS}}">
|
<link rel="stylesheet" href="{{.CustomCSS}}">
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if $.Ctx.FluorideMode}}
|
{{if $.Ctx.FluorideMode}}
|
||||||
<script src="/static/fluoride.js"></script>
|
<script src="/static/fluoride.js"></script>
|
||||||
|
|
|
@ -16,6 +16,10 @@ func NewRandID(n int) (string, error) {
|
||||||
return enc.EncodeToString(data), nil
|
return enc.EncodeToString(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewSessionID() (string, error) {
|
||||||
|
return NewRandID(24)
|
||||||
|
}
|
||||||
|
|
||||||
func NewCSRFToken() (string, error) {
|
func NewCSRFToken() (string, error) {
|
||||||
return NewRandID(24)
|
return NewRandID(24)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue