Compare commits

...

17 Commits
B1.0 ... main

Author SHA1 Message Date
Romain de Laage 87784a8166
Hotfix: Reblog content were not visible 2022-05-11 15:28:11 +02:00
Romain de Laage 88707930b9
Bump to 1.2 2022-04-30 14:33:12 +02:00
Romain de Laage 52103bd76d
Add support for media proxying and use io instead of deprecated io/ioutil 2022-04-30 14:32:28 +02:00
Romain de Laage 1ae21d1c2b
Add support for timeline 2022-04-29 10:35:43 +02:00
Romain de Laage 1c5ead27b0
Add support for reblog 2021-05-02 19:30:47 +02:00
Romain de Laage 8494120df0
Bump to 1.1 in makefile 2021-04-18 10:48:43 +02:00
Romain de Laage 5ba39fbdb8
Bump version to 1.1 2021-04-18 10:44:57 +02:00
Romain de Laage 92ee4fe2bd
Make rate limite configurable, little entrypoint fix 2021-04-18 10:43:12 +02:00
Romain de Laage de4113154d
Add rate limit 2021-04-17 12:10:27 +02:00
Romain de Laage 545ebe8321
Add newline before tags title in toot info 2021-03-05 10:48:30 +01:00
Romain de Laage d95b1bbccc
Handle json parsing error with masto API, return error with getBlog 2021-03-05 10:46:31 +01:00
Romain de Laage b6b7fd1cb5
Handle error when parsing json config file 2021-03-05 10:32:55 +01:00
Romain de Laage 17b538f297
Add docker image 2021-03-04 19:32:46 +01:00
Romain de Laage abd5b21817
Add toot info page and improve linking between pages 2021-03-04 10:13:38 +01:00
Romain de Laage ce86ff0386
Toot title is less important (h3), decode url query for tag, use account handle instead of username 2021-03-04 09:28:43 +01:00
Romain de Laage 8aad570ab4
Update toot printing 2021-03-03 16:30:26 +01:00
Romain de Laage cd7c29c302
Add mastodon instance mention in about page 2021-03-03 12:03:41 +01:00
10 changed files with 666 additions and 64 deletions

20
Dockerfile Normal file
View File

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

View File

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

View File

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

View File

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

15
docker-compose.yml Normal file
View File

@ -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
View File

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

View File

@ -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
View File

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

51
start.sh Normal file
View File

@ -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
View File

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