Compare commits
17 Commits
Author | SHA1 | Date |
---|---|---|
Romain de Laage | 87784a8166 | |
Romain de Laage | 88707930b9 | |
Romain de Laage | 52103bd76d | |
Romain de Laage | 1ae21d1c2b | |
Romain de Laage | 1c5ead27b0 | |
Romain de Laage | 8494120df0 | |
Romain de Laage | 5ba39fbdb8 | |
Romain de Laage | 92ee4fe2bd | |
Romain de Laage | de4113154d | |
Romain de Laage | 545ebe8321 | |
Romain de Laage | d95b1bbccc | |
Romain de Laage | b6b7fd1cb5 | |
Romain de Laage | 17b538f297 | |
Romain de Laage | abd5b21817 | |
Romain de Laage | ce86ff0386 | |
Romain de Laage | 8aad570ab4 | |
Romain de Laage | cd7c29c302 |
|
@ -0,0 +1,20 @@
|
|||
FROM golang:1.16.0-buster as BUILD
|
||||
|
||||
COPY . mastogem
|
||||
|
||||
RUN cd mastogem && \
|
||||
go build -o /mastogem
|
||||
|
||||
FROM debian:buster-slim
|
||||
|
||||
COPY --from=BUILD /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
||||
|
||||
COPY --from=BUILD /mastogem /mastogem
|
||||
|
||||
COPY start.sh /start.sh
|
||||
|
||||
RUN chmod +x /start.sh
|
||||
|
||||
EXPOSE 1965
|
||||
|
||||
CMD "/start.sh"
|
12
Makefile
12
Makefile
|
@ -1,9 +1,15 @@
|
|||
all: amd64 arm7
|
||||
VERSION=1.2
|
||||
SOURCES=$(shell find . -name "*.go" -type f)
|
||||
|
||||
amd64:
|
||||
all: amd64 arm7 dockerimage
|
||||
|
||||
amd64: $(SOURCES)
|
||||
mkdir -p build
|
||||
GOOS=linux GOARCH=amd64 go build -o build/mastogem-amd64
|
||||
|
||||
arm7:
|
||||
arm7: $(SOURCES)
|
||||
mkdir -p build
|
||||
GOOS=linux GOARCH=arm GOARM=7 go build -o build/mastogem-arm7
|
||||
|
||||
dockerimage: Dockerfile start.sh $(SOURCES)
|
||||
sudo docker build -t dervom/mastogem:$(VERSION) .
|
||||
|
|
23
README.md
23
README.md
|
@ -31,6 +31,29 @@ You should provide the `MASTOGEM_CONFIG_PATH` environment variable when launchin
|
|||
|
||||
To run the program simply run the executable file corresponding to your architecture in the `build` folder. Make sure the certificate and the key where generated before.
|
||||
|
||||
## Run with Docker
|
||||
|
||||
You could use docker or docker-compose to run this program, you must set a `MASTODON_BASE_URL` variable corresponding to the URL to your Mastodon instance (without the tailing slash), you also must bind a certificate (`/cert.pem`) and a key (`/key.rsa`).
|
||||
|
||||
You can set a `TITLE` and a `HOME_MESSAGE` variables.
|
||||
|
||||
### Docker
|
||||
|
||||
```
|
||||
sudo docker run -it \
|
||||
-p 1965:1965 \
|
||||
-e MASTODON_BASE_URL=https://mamot.fr \
|
||||
--mount type=bind,source=$(pwd)/certs/cert.pem,destination=/cert.pem \
|
||||
--mount type=bind,source=$(pwd)/certs/key.rsa,destination=/key.rsa \
|
||||
dervom/mastogem
|
||||
```
|
||||
|
||||
### Docker-compose
|
||||
|
||||
```
|
||||
sudo docker-compose up -d
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
You contributions are welcomed, you can send me an email (romain.delaage@rdelaage.ovh).
|
||||
|
|
|
@ -4,5 +4,6 @@
|
|||
"key_path": "certs/key.rsa",
|
||||
"base_url": "https://mamot.fr",
|
||||
"title": "MastoGem",
|
||||
"home_message": "Welcome on MastoGem, a Mastodon proxy for Gemini.\nYou can view the last 20 toots of a Mastodon account by providing its id, for example:\n=> gemini://localhost/310515 My Mastodon account"
|
||||
"home_message": "Welcome on MastoGem, a Mastodon proxy for Gemini.\nYou can view the last 20 toots of a Mastodon account by providing its id, for example:\n=> gemini://localhost/310515 My Mastodon account",
|
||||
"rate_limit": 45
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
version: "3.7"
|
||||
|
||||
services:
|
||||
mastogem:
|
||||
image: dervom/mastogem:1.1
|
||||
build: .
|
||||
container_name: mastogem
|
||||
volumes:
|
||||
- ./certs/key.rsa:/key.rsa:ro
|
||||
- ./certs/cert.pem:/cert.pem:ro
|
||||
environment:
|
||||
MASTODON_BASE_URL: "https://mamot.fr"
|
||||
ports:
|
||||
- 1965:1965
|
||||
restart: unless-stopped
|
48
main.go
48
main.go
|
@ -17,13 +17,24 @@
|
|||
*/
|
||||
package main
|
||||
|
||||
import "log"
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Blog struct {
|
||||
Id string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Date string `json:"created_at"`
|
||||
Author Account `json:"account"`
|
||||
Id string `json:"id"`
|
||||
Content string `json:"content"`
|
||||
Date string `json:"created_at"`
|
||||
Author Account `json:"account"`
|
||||
Tags []Tag `json:"tags"`
|
||||
Mentions []Mention `json:"mentions"`
|
||||
Reblog *Blog `json:"reblog"`
|
||||
Medias []Media `json:"media_attachments"`
|
||||
}
|
||||
|
||||
type Media struct {
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
|
@ -33,12 +44,14 @@ type Config struct {
|
|||
BaseURL string `json:"base_url"`
|
||||
Title string `json:"title"`
|
||||
HomeMessage string `json:"home_message"`
|
||||
RateLimit int `json:"rate_limit"`
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"display_name"`
|
||||
Url string `json:"url"`
|
||||
Id string `json:"id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Name string `json:"acct"`
|
||||
Url string `json:"url"`
|
||||
}
|
||||
|
||||
type Thread struct {
|
||||
|
@ -46,12 +59,29 @@ type Thread struct {
|
|||
Descendants []Blog `json:"descendants"`
|
||||
}
|
||||
|
||||
type Mention struct {
|
||||
Id string `json:"id"`
|
||||
Name string `json:"acct"`
|
||||
}
|
||||
|
||||
type Tag struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Rate struct {
|
||||
Date time.Time
|
||||
Count int
|
||||
}
|
||||
|
||||
var rateMap map[string]Rate
|
||||
|
||||
func main() {
|
||||
config := getConfig()
|
||||
rateMap = make(map[string]Rate)
|
||||
|
||||
listener := listen(config.Listen, config.CertPath, config.KeyPath)
|
||||
log.Println("Server successfully started")
|
||||
log.Println("Server is listening at " + config.Listen)
|
||||
|
||||
serve(listener, config.BaseURL, config.Title, config.HomeMessage)
|
||||
serve(listener, config.BaseURL, config.Title, config.HomeMessage, config.RateLimit)
|
||||
}
|
||||
|
|
154
mastoUtil.go
154
mastoUtil.go
|
@ -21,38 +21,114 @@ import (
|
|||
"net/http"
|
||||
"log"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func getBlog(baseURL, account string) []Blog {
|
||||
func getBlogAndReblog(baseURL, account string) ([]Blog, error) {
|
||||
if baseURL == "" || account == "" {
|
||||
log.Println("baseURL or account is empty")
|
||||
return nil
|
||||
return nil, fmt.Errorf("BaseURL or account is empty")
|
||||
}
|
||||
|
||||
resp, err := http.Get(baseURL + "/api/v1/accounts/" + account + "/statuses?exclude_reblogs=true&exclude_replies=true")
|
||||
resp, err := http.Get(baseURL + "/api/v1/accounts/" + account + "/statuses?exclude_reblogs=false&exclude_replies=true")
|
||||
if err != nil {
|
||||
log.Println("Mastodon API request: %s", err)
|
||||
return nil
|
||||
return nil, fmt.Errorf("Failed to request Mastodon API")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Println("Mastodon API response: %s", resp.Status)
|
||||
return nil
|
||||
return nil, fmt.Errorf("Mastodon instance failed")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response body: %s", err)
|
||||
return nil, fmt.Errorf("Failed to read Mastodon response body")
|
||||
}
|
||||
|
||||
var blogs []Blog
|
||||
json.Unmarshal(body, &blogs)
|
||||
err = json.Unmarshal(body, &blogs)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response %s", err)
|
||||
return nil, fmt.Errorf("Failed to parse Mastodon response")
|
||||
}
|
||||
|
||||
return blogs
|
||||
return blogs, nil
|
||||
}
|
||||
|
||||
func getBlog(baseURL, account string) ([]Blog, error) {
|
||||
if baseURL == "" || account == "" {
|
||||
log.Println("baseURL or account is empty")
|
||||
return nil, fmt.Errorf("BaseURL or account is empty")
|
||||
}
|
||||
|
||||
resp, err := http.Get(baseURL + "/api/v1/accounts/" + account + "/statuses?exclude_reblogs=true&exclude_replies=true")
|
||||
if err != nil {
|
||||
log.Println("Mastodon API request: %s", err)
|
||||
return nil, fmt.Errorf("Failed to request Mastodon API")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Println("Mastodon API response: %s", resp.Status)
|
||||
return nil, fmt.Errorf("Mastodon instance failed")
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response body: %s", err)
|
||||
return nil, fmt.Errorf("Failed to read Mastodon response body")
|
||||
}
|
||||
|
||||
var blogs []Blog
|
||||
err = json.Unmarshal(body, &blogs)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response %s", err)
|
||||
return nil, fmt.Errorf("Failed to parse Mastodon response")
|
||||
}
|
||||
|
||||
return blogs, nil
|
||||
}
|
||||
|
||||
func getTimeline(baseURL string) ([]Blog, error) {
|
||||
var toots []Blog
|
||||
|
||||
if baseURL == "" {
|
||||
log.Println("baseURL is empty")
|
||||
return toots, fmt.Errorf("baseURL is empty")
|
||||
}
|
||||
|
||||
resp, err := http.Get(baseURL + "/api/v1/timelines/public")
|
||||
if err != nil {
|
||||
log.Println("Mastodon API request: %s", err)
|
||||
return toots, fmt.Errorf("API request failed")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Println("Mastodon API response: %s", resp.Status)
|
||||
return toots, fmt.Errorf("API response is not 200")
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response body: %s", err)
|
||||
return toots, fmt.Errorf("Failed to read response")
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &toots)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response %s", err)
|
||||
return toots, fmt.Errorf("Failed to parse response")
|
||||
}
|
||||
|
||||
return toots, nil
|
||||
}
|
||||
|
||||
func getAccount(baseURL, accountId string) (Account, error) {
|
||||
|
@ -74,14 +150,18 @@ func getAccount(baseURL, accountId string) (Account, error) {
|
|||
return Account{}, fmt.Errorf("API response is not 200")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response body: %s", err)
|
||||
return Account{}, fmt.Errorf("Failed to read response")
|
||||
}
|
||||
|
||||
var account Account
|
||||
json.Unmarshal(body, &account)
|
||||
err = json.Unmarshal(body, &account)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response %s", err)
|
||||
return Account{}, fmt.Errorf("Failed to parse response")
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
@ -105,18 +185,50 @@ func getToot(baseURL, tootId string) (Blog, error) {
|
|||
return Blog{}, fmt.Errorf("API response is not 200")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response body: %s", err)
|
||||
return Blog{}, fmt.Errorf("Failed to read response")
|
||||
}
|
||||
|
||||
var toot Blog
|
||||
json.Unmarshal(body, &toot)
|
||||
err = json.Unmarshal(body, &toot)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response %s", err)
|
||||
return Blog{}, fmt.Errorf("Failed to parse response")
|
||||
}
|
||||
|
||||
return toot, nil
|
||||
}
|
||||
|
||||
func getMedia(mediaURL string) (string, []byte, error) {
|
||||
if mediaURL == "" {
|
||||
log.Println("mediaURL is empty")
|
||||
return "", nil, fmt.Errorf("mediaURL is empty")
|
||||
}
|
||||
|
||||
resp, err := http.Get(mediaURL)
|
||||
if err != nil {
|
||||
log.Println("Mastodon API request: %s", err)
|
||||
return "", nil, fmt.Errorf("API request failed")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Println("Mastodon API response: %s", resp.Status)
|
||||
return "", nil, fmt.Errorf("API response is not 200")
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response body: %s", err)
|
||||
return "", nil, fmt.Errorf("Failed to read response")
|
||||
}
|
||||
|
||||
return resp.Header["Content-Type"][0], body , nil
|
||||
}
|
||||
|
||||
func getThread(baseURL, tootId string) (Thread, error) {
|
||||
if baseURL == "" || tootId == "" {
|
||||
log.Println("baseURL or tootID is empty")
|
||||
|
@ -136,14 +248,18 @@ func getThread(baseURL, tootId string) (Thread, error) {
|
|||
return Thread{}, fmt.Errorf("API response is not 200")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response body: %s", err)
|
||||
return Thread{}, fmt.Errorf("Failed to read response")
|
||||
}
|
||||
|
||||
var thread Thread
|
||||
json.Unmarshal(body, &thread)
|
||||
err = json.Unmarshal(body, &thread)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response %s", err)
|
||||
return Thread{}, fmt.Errorf("Failed to parse response")
|
||||
}
|
||||
|
||||
return thread, nil
|
||||
}
|
||||
|
@ -167,14 +283,18 @@ func getTag(baseURL, tag string) ([]Blog, error) {
|
|||
return nil, fmt.Errorf("API response is not 200")
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response body: %s", err)
|
||||
return nil, fmt.Errorf("Failed to read response")
|
||||
}
|
||||
|
||||
var blogs []Blog
|
||||
json.Unmarshal(body, &blogs)
|
||||
err = json.Unmarshal(body, &blogs)
|
||||
if err != nil {
|
||||
log.Println("Mastodon response: %s", err)
|
||||
return nil, fmt.Errorf("Failed to parse response")
|
||||
}
|
||||
|
||||
return blogs, nil
|
||||
}
|
||||
|
|
329
server.go
329
server.go
|
@ -48,14 +48,14 @@ func listen(address, certFile, keyFile string) net.Listener {
|
|||
return listener
|
||||
}
|
||||
|
||||
func serve(listener net.Listener, baseURL, title, home_message string) {
|
||||
func serve(listener net.Listener, baseURL, title, home_message string, rateLimit int) {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
go handleConn(conn.(*tls.Conn), baseURL, title, home_message)
|
||||
go handleConn(conn.(*tls.Conn), baseURL, title, home_message, rateLimit)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,9 +87,19 @@ func getPath(conn *tls.Conn) (string, string, error) {
|
|||
return parsedURL.Path, parsedURL.RawQuery, nil
|
||||
}
|
||||
|
||||
func handleConn(conn *tls.Conn, baseURL, title, home_message string) {
|
||||
func handleConn(conn *tls.Conn, baseURL, title, home_message string, rateLimit int) {
|
||||
defer conn.Close()
|
||||
|
||||
if !rateIsOk(rateMap, strings.Split(conn.RemoteAddr().String(), ":")[0], rateLimit) {
|
||||
log.Printf("Too many requests for %s\n", conn.RemoteAddr().String())
|
||||
_, err := fmt.Fprintf(conn, "44 60\r\n")
|
||||
if err != nil {
|
||||
log.Println("send error: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
path, query, err := getPath(conn)
|
||||
if err != nil {
|
||||
log.Println("get url: %s", err)
|
||||
|
@ -103,7 +113,9 @@ func handleConn(conn *tls.Conn, baseURL, title, home_message string) {
|
|||
|
||||
// home
|
||||
if path == "" || path == "/" {
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# " + title + "\n\n" + home_message + "\n\n=> /tag Search for a tag\n=> /about About MastoGem")
|
||||
log.Println("Received request for home page")
|
||||
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# %s\n\n%s\n\n=> /timeline View public timeline\n=> /tag Search for a tag\n=> /about About MastoGem", title, home_message)
|
||||
if err != nil {
|
||||
log.Println("send error: %s", err)
|
||||
return
|
||||
|
@ -113,22 +125,39 @@ func handleConn(conn *tls.Conn, baseURL, title, home_message string) {
|
|||
|
||||
// profile
|
||||
if strings.HasPrefix(path, "/profile/") {
|
||||
// skip prefix
|
||||
path = path[9:]
|
||||
_, err = strconv.ParseUint(path, 10, 64)
|
||||
if err != nil {
|
||||
log.Println("invalid request: %s", err)
|
||||
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
||||
if strings.HasSuffix(path, "/reblog") {
|
||||
// skip prefix and suffix
|
||||
path = path[9:len(path)-len("/reblog")]
|
||||
_, err = strconv.ParseUint(path, 10, 64)
|
||||
if err != nil {
|
||||
log.Println("send error: %s", err)
|
||||
log.Println("invalid request: %s", err)
|
||||
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
||||
if err != nil {
|
||||
log.Println("send error: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
|
||||
log.Println("Received request for account with reblog " + path)
|
||||
printProfileWithReblog(conn, baseURL, path)
|
||||
} else {
|
||||
// skip prefix
|
||||
path = path[9:]
|
||||
_, err = strconv.ParseUint(path, 10, 64)
|
||||
if err != nil {
|
||||
log.Println("invalid request: %s", err)
|
||||
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
||||
if err != nil {
|
||||
log.Println("send error: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Received request for account " + path)
|
||||
printProfile(conn, baseURL, path)
|
||||
}
|
||||
|
||||
log.Println("Received request for account " + path)
|
||||
|
||||
printProfile(conn, baseURL, path)
|
||||
} /* thread */ else if strings.HasPrefix(path, "/thread/") {
|
||||
// skip prefix
|
||||
path = path[8:]
|
||||
|
@ -156,10 +185,17 @@ This capsule is running MastoGem, a free (as in free speech, not as in free beer
|
|||
|
||||
Feel free to contribute, send feedback or share ideas.
|
||||
|
||||
# Mastodon instance
|
||||
|
||||
This capsule use %s Mastodon instance.
|
||||
|
||||
# AGPLv3 License
|
||||
|
||||
=> http://www.gnu.org/licenses/agpl-3.0.txt AGPLv3 License text`
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n" + page)
|
||||
|
||||
log.Println("Received request for about page")
|
||||
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n" + page, baseURL)
|
||||
if err != nil {
|
||||
log.Println("send: %s", err)
|
||||
return
|
||||
|
@ -174,7 +210,43 @@ Feel free to contribute, send feedback or share ideas.
|
|||
return
|
||||
}
|
||||
|
||||
log.Println("Received request for tag " + query)
|
||||
|
||||
printTag(conn, baseURL, query)
|
||||
} /* toot */ else if strings.HasPrefix(path, "/toot/") {
|
||||
path = path[6:]
|
||||
|
||||
_, err = strconv.ParseUint(path, 10, 64)
|
||||
if err != nil {
|
||||
log.Println("invalid request: %s", err)
|
||||
_, err = fmt.Fprintf(conn, "59 Can't parse request\r\n")
|
||||
if err != nil {
|
||||
log.Println("send error: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Received request for toot " + path)
|
||||
|
||||
printToot(conn, baseURL, path)
|
||||
} /* timeline */ else if strings.HasPrefix(path, "/timeline") {
|
||||
log.Println("Received request for timeline")
|
||||
|
||||
printTimeline(conn, baseURL)
|
||||
} /* media */ else if strings.HasPrefix(path, "/media") {
|
||||
if query == "" {
|
||||
_, err = fmt.Fprintf(conn, "59 Invalid request\r\n")
|
||||
if err != nil {
|
||||
log.Println("send: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Received request for media " + query)
|
||||
|
||||
proxyMedia(conn, baseURL, query)
|
||||
} else {
|
||||
_, err = fmt.Fprintf(conn, "59 Invalid request\r\n")
|
||||
if err != nil {
|
||||
|
@ -184,11 +256,28 @@ Feel free to contribute, send feedback or share ideas.
|
|||
}
|
||||
}
|
||||
|
||||
func printProfile(conn *tls.Conn, baseURL, profileID string) {
|
||||
account, err := getAccount(baseURL, profileID)
|
||||
blogs := getBlog(baseURL, profileID)
|
||||
func proxyMedia(conn *tls.Conn, baseURL, query string) {
|
||||
mediaURL, err := url.QueryUnescape(query)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "59 Invalid url encoded media\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil || blogs == nil {
|
||||
if !strings.HasPrefix(mediaURL, baseURL) {
|
||||
_, err = fmt.Fprintf(conn, "59 Invalid media url\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
mime, media, err := getMedia(mediaURL)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
|
@ -197,31 +286,194 @@ func printProfile(conn *tls.Conn, baseURL, profileID string) {
|
|||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Toots for " + account.Name + " account\n")
|
||||
_, err = fmt.Fprintf(conn, "20 %s\r\n", mime)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = conn.Write(media)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func printProfile(conn *tls.Conn, baseURL, profileID string) {
|
||||
account, err := getAccount(baseURL, profileID)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
blogs, err := getBlog(baseURL, profileID)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Toots for %s account\n", account.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, blog := range blogs {
|
||||
date := "\n```\n* Posted at " + blog.Date + " *\n```\n"
|
||||
|
||||
text := removeHTMLTags(blog.Content) + "\n"
|
||||
|
||||
_, err = fmt.Fprintf(conn, date + text + "=> /thread/" + blog.Id + " View the thread\n")
|
||||
_, err = fmt.Fprintf(conn, "\n%s\n=> /thread/%s View the thread\n", formatBlog(blog), blog.Id)
|
||||
if err != nil {
|
||||
log.Println("read blogs: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(conn, "=> " + account.Url + " Go to " + account.Name + " account")
|
||||
_, err = fmt.Fprintf(conn, "\n=> /profile/%s/reblog This profile with reblog\n=> %s Go to %s account", account.Id, account.Url, account.Name)
|
||||
if err != nil {
|
||||
log.Println("add link: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func printProfileWithReblog(conn *tls.Conn, baseURL, profileID string) {
|
||||
account, err := getAccount(baseURL, profileID)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
blogs, err := getBlogAndReblog(baseURL, profileID)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Toots for %s account\n", account.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, blog := range blogs {
|
||||
_, err = fmt.Fprintf(conn, "\n%s\n=> /thread/%s View the thread\n", formatBlog(blog), blog.Id)
|
||||
if err != nil {
|
||||
log.Println("read blogs: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintf(conn, "\n=> /profile/%s This profile without reblog\n=> %s Go to %s account", account.Id, account.Url, account.Name)
|
||||
if err != nil {
|
||||
log.Println("add link: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func printTimeline(conn *tls.Conn, baseURL string) {
|
||||
toots, err := getTimeline(baseURL)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Print header
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Print toots
|
||||
for _, toot := range toots {
|
||||
_, err = fmt.Fprintf(conn, "\n%s\n=> /thread/%s View the thread\n", formatBlog(toot), toot.Id)
|
||||
if err != nil {
|
||||
log.Println("read timeline: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printToot(conn *tls.Conn, baseURL, tootID string) {
|
||||
toot, err := getToot(baseURL, tootID)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "40 Remote mastodon instance failed\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Print header
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Print toot
|
||||
if toot.Reblog == nil {
|
||||
_, err = fmt.Fprintf(conn, "# Toot\n\n%s\n=> /thread/%s View the thread\n=> /profile/%s More toots from %s\n", formatBlog(toot), toot.Id, toot.Author.Id, toot.Author.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
_, err = fmt.Fprintf(conn, "# Toot\n\n%s\n=> /toot/%s Original toot\n=> /thread/%s View the thread\n=> /profile/%s More toots from %s\n=> /profile/%s More toots from %s\n", formatBlog(toot), toot.Reblog.Id, toot.Id, toot.Author.Id, toot.Author.Name, toot.Reblog.Author.Id, toot.Reblog.Author.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// print mentions
|
||||
_, err = fmt.Fprintf(conn, "\n# Mentions\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, mention := range toot.Mentions {
|
||||
_, err = fmt.Fprintf(conn, "\n=> /profile/%s View %s profile", mention.Id, mention.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
// print tags
|
||||
_, err = fmt.Fprintf(conn, "\n\n# Tags\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, tag := range toot.Tags {
|
||||
_, err = fmt.Fprintf(conn, "\n=> /tag?%s View %s tag", url.QueryEscape(tag.Name), tag.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func printThread(conn *tls.Conn, baseURL, tootID string) {
|
||||
originalToot, err := getToot(baseURL, tootID)
|
||||
if err != nil {
|
||||
|
@ -252,7 +504,7 @@ func printThread(conn *tls.Conn, baseURL, tootID string) {
|
|||
|
||||
// Print each anscestor
|
||||
for _, toot := range thread.Ancestors {
|
||||
_, err = fmt.Fprintf(conn, "\n```\n* Posted on " + toot.Date + " by " + toot.Author.Name + " *\n```\n" + removeHTMLTags(toot.Content) + "\n=> /profile/" + toot.Author.Id + " More toots from " + toot.Author.Name + "\n")
|
||||
_, err = fmt.Fprintf(conn, "\n%s\n=> /profile/%s More toots from %s\n", formatBlog(toot), toot.Author.Id, toot.Author.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
|
@ -260,7 +512,7 @@ func printThread(conn *tls.Conn, baseURL, tootID string) {
|
|||
}
|
||||
|
||||
// Print original toot
|
||||
_, err = fmt.Fprintf(conn, "\n# Toot\n\n```\n* Posted on " + originalToot.Date + " by "+ originalToot.Author.Name +" *\n```\n" + removeHTMLTags(originalToot.Content) + "\n=> /profile/" + originalToot.Author.Id + " More toots from " + originalToot.Author.Name + "\n")
|
||||
_, err = fmt.Fprintf(conn, "\n# Toot\n\n%s\n=> /profile/%s More toots from %s\n", formatBlog(originalToot), originalToot.Author.Id, originalToot.Author.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
|
@ -273,7 +525,7 @@ func printThread(conn *tls.Conn, baseURL, tootID string) {
|
|||
return
|
||||
}
|
||||
for _, toot := range thread.Descendants {
|
||||
_, err = fmt.Fprintf(conn, "\n```\n* Posted on " + toot.Date + " by " + toot.Author.Name + " *\n```\n" + removeHTMLTags(toot.Content) + "\n=> /profile/" + toot.Author.Id + " More toots from " + toot.Author.Name + "\n")
|
||||
_, err = fmt.Fprintf(conn, "\n%s\n=> /profile/%s More toots from %s\n", formatBlog(toot), toot.Author.Id, toot.Author.Name)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
|
@ -292,8 +544,19 @@ func printTag(conn *tls.Conn, baseURL, tag string) {
|
|||
return
|
||||
}
|
||||
|
||||
//decode tag (url encoded char like space)
|
||||
tag, err = url.QueryUnescape(tag)
|
||||
if err != nil {
|
||||
_, err = fmt.Fprintf(conn, "59 Invalid url encoded tag\r\n")
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Print header
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Toots for " + tag + "\n")
|
||||
_, err = fmt.Fprintf(conn, "20 text/gemini\r\n# Toots for %s\n", tag)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
|
@ -301,7 +564,7 @@ func printTag(conn *tls.Conn, baseURL, tag string) {
|
|||
|
||||
// Print toots
|
||||
for _, toot := range toots {
|
||||
_, err = fmt.Fprintf(conn, "\n```\n* Posted on " + toot.Date + " by " + toot.Author.Name + " *\n```\n" + removeHTMLTags(toot.Content) + "\n=> /profile/" + toot.Author.Id + " More toots from " + toot.Author.Name + "\n")
|
||||
_, err = fmt.Fprintf(conn, "\n%s\n=> /profile/%s More toots from %s\n=> /thread/%s View the thread\n", formatBlog(toot), toot.Author.Id, toot.Author.Name, toot.Id)
|
||||
if err != nil {
|
||||
log.Println("handleConn: %s", err)
|
||||
return
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
#! /bin/sh
|
||||
|
||||
if [ -z "$MASTODON_BASE_URL" ]
|
||||
then
|
||||
echo "You must set the MASTODON_BASE_URL variable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f /cert.pem ]
|
||||
then
|
||||
echo "You must bind a certificate at /cert.pem"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f /key.rsa ]
|
||||
then
|
||||
echo "You must bind a private key at /key.rsa"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$TITLE" ]
|
||||
then
|
||||
echo "Using default title"
|
||||
TITLE=MastoGem
|
||||
fi
|
||||
|
||||
if [ -z "$HOME_MESSAGE" ]
|
||||
then
|
||||
echo "Using default home message"
|
||||
HOME_MESSAGE="Welcome on MastoGem, a Mastodon proxy for Gemini !"
|
||||
fi
|
||||
|
||||
if [ -z "$RATE_LIMIT" ]
|
||||
then
|
||||
echo "Using default rate limit"
|
||||
RATE_LIMIT=45
|
||||
fi
|
||||
|
||||
cat << EOF > /config.json
|
||||
{
|
||||
"listen": "0.0.0.0:1965",
|
||||
"cert_path": "/cert.pem",
|
||||
"key_path": "/key.rsa",
|
||||
"base_url": "$MASTODON_BASE_URL",
|
||||
"title": "$TITLE",
|
||||
"home_message": "$HOME_MESSAGE",
|
||||
"rate_limit": $RATE_LIMIT
|
||||
}
|
||||
EOF
|
||||
|
||||
MASTOGEM_CONFIG_PATH=/config.json /mastogem
|
75
util.go
75
util.go
|
@ -25,6 +25,8 @@ import (
|
|||
"io/ioutil"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"time"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func getConfig() Config {
|
||||
|
@ -39,6 +41,7 @@ func getConfig() Config {
|
|||
BaseURL: "https://mamot.fr",
|
||||
Title: "MastoGem",
|
||||
HomeMessage: "Welcome on MastoGem, this is a Mastodon proxy for Gemini. You can view the last 20 toots of a Mastodon account by providing its ID.",
|
||||
RateLimit: 45,
|
||||
}
|
||||
|
||||
return config
|
||||
|
@ -51,7 +54,10 @@ func getConfig() Config {
|
|||
|
||||
var config Config
|
||||
|
||||
json.Unmarshal(configFile, &config)
|
||||
err = json.Unmarshal(configFile, &config)
|
||||
if err != nil {
|
||||
log.Fatalln("config file: %s", err)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
@ -81,3 +87,70 @@ func removeHTMLTags(content string) string {
|
|||
|
||||
return text
|
||||
}
|
||||
|
||||
func formatBlog(toot Blog) string {
|
||||
var content string
|
||||
if toot.Reblog == nil {
|
||||
content = toot.Content
|
||||
} else {
|
||||
content = toot.Reblog.Content
|
||||
}
|
||||
content = removeHTMLTags(content)
|
||||
content = strings.Trim(content, " \n\r")
|
||||
content = strings.ReplaceAll(content, "\n#", "\n[#]")
|
||||
if strings.HasPrefix(content, "#") {
|
||||
content = "[#]" + content[1:]
|
||||
}
|
||||
|
||||
var author string
|
||||
|
||||
if toot.Author.DisplayName == "" {
|
||||
author = toot.Author.Name
|
||||
} else {
|
||||
author = toot.Author.DisplayName
|
||||
}
|
||||
|
||||
var header string
|
||||
|
||||
if toot.Reblog == nil {
|
||||
header = "### Written by " + author + " on " + toot.Date[0:10] + " at " + toot.Date[11:16]
|
||||
} else {
|
||||
var originalAuthor string
|
||||
if toot.Reblog.Author.DisplayName == "" {
|
||||
originalAuthor = toot.Reblog.Author.Name
|
||||
} else {
|
||||
originalAuthor = toot.Reblog.Author.DisplayName
|
||||
}
|
||||
|
||||
header = "### Shared by " + author + " on " + toot.Date[0:10] + " at " + toot.Date[11:16] + " (original by " + originalAuthor + ")"
|
||||
}
|
||||
|
||||
footer := "\n"
|
||||
for _, media := range toot.Medias {
|
||||
mediaURL := url.QueryEscape(media.Url)
|
||||
footer += "=> /media?" + mediaURL + " View attached media\n"
|
||||
}
|
||||
footer += "\n=> /toot/" + toot.Id + " More informations about this toot"
|
||||
|
||||
return header + "\n" + content + "\n" + footer
|
||||
}
|
||||
|
||||
func rateIsOk(tab map[string]Rate, remoteIP string, limit int) bool {
|
||||
elmt, ok := tab[remoteIP]
|
||||
if ok == false {
|
||||
tab[remoteIP] = Rate{time.Now(), 1}
|
||||
return true
|
||||
} else {
|
||||
if time.Since(elmt.Date).Minutes() >= 1 {
|
||||
tab[remoteIP] = Rate{time.Now(), 1}
|
||||
return true
|
||||
} else {
|
||||
if elmt.Count < limit {
|
||||
tab[remoteIP] = Rate{elmt.Date, elmt.Count + 1}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue