~athorp96/athena

ce02b247deab1fafb0357a53688780bcf39cc241 — Andrew Thorp 3 years ago
Initial commit
8 files changed, 230 insertions(+), 0 deletions(-)

A .gitignore
A LICENSE
A README.md
A go.mod
A go.sum
A main.go
A public/footer.gmi
A public/header.gmi
A  => .gitignore +2 -0
@@ 1,2 @@
*.crt
*.key

A  => LICENSE +13 -0
@@ 1,13 @@
Copyright 2021 Andrew Thorp

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

A  => README.md +6 -0
@@ 1,6 @@
# Athena

Dictionary service for the Gemini space.

This dictionary service is powered by [Free Dictionary API](https://dictionaryapi.dev/).
Please support them and their server costs.

A  => go.mod +10 -0
@@ 1,10 @@
module git.sr.ht/~athorp96/athena

go 1.16

require (
	github.com/a-h/gemini v0.0.61
	github.com/spf13/pflag v1.0.5
)

replace github.com/a-h/gemini => ../go-gemini

A  => go.sum +2 -0
@@ 1,2 @@
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=

A  => main.go +181 -0
@@ 1,181 @@
package main

import (
	"bufio"
	"context"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"os"
	"strings"

	"github.com/a-h/gemini"
	"github.com/a-h/gemini/mux"
	"github.com/spf13/pflag"
)

// Configuration struct.
type athenaConfig struct {
	domain   string
	port     uint
	certPath string
	keyPath  string
}

func configure() athenaConfig {
	const domainHelp string = "Domain to serve. Should match the certificate."
	const portHelp string = "Port to listen on. If not fronted by a proxy server, should be 1965."
	const certPathHelp string = "Path to certificate file."
	const keyPathHelp string = "Path to key file."

	config := athenaConfig{}

	pflag.StringVarP(&config.domain, "domain", "d", "localhost", domainHelp)
	pflag.UintVarP(&config.port, "port", "p", 1965, portHelp)
	pflag.StringVarP(&config.certPath, "certPath", "c", "server.crt", certPathHelp)
	pflag.StringVarP(&config.keyPath, "keyPath", "k", "server.key", keyPathHelp)

	pflag.Parse()

	return config
}

type Definition struct {
	Definition string   `json:"definition"`
	Example    string   `json:"example"`
	Synonyms   []string `json:"synonyms"`
}
type Meanings struct {
	PartOfSpeech string       `json:"partOfSpeech"`
	Definitions  []Definition `json:"definitions"`
}
type Word struct {
	Word      string              `json:"word"`
	Phonetics []map[string]string `json:"phonetics"`
	Meanings  []Meanings          `json:"meanings"`
}

func getFileContent(path string) string {
	file, err := os.Open(path)
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	scanner := bufio.NewScanner(file)
	content := ""
	for scanner.Scan() {
		content += scanner.Text() + "\n"
	}

	if err := scanner.Err(); err != nil {
		log.Fatal(err)
	}
	return content
}

func newAthenaDocument() gemini.DocumentBuilder {
	page := gemini.NewDocumentBuilder()
	page.SetHeader(getFileContent("./public/header.gmi"))
	page.AddLine("---")
	page.SetFooter(getFileContent("./public/footer.gmi"))
	return page
}

func definitionToPage(data []byte) []byte {
	page := newAthenaDocument()

	var payload []Word
	err := json.Unmarshal(data, &payload)
	if err != nil {
		page.AddLine("An unknown error has occored...")
		log.Println("Error unmarshalling response: %s", err)
	}

	for i := 0; i < len(payload); i++ {
		word := payload[i]

		page.AddLine("")
		page.AddH1Header(strings.Title(word.Word))

		for j := 0; j < len(word.Phonetics); j++ {
			phonetic := word.Phonetics[j]
			page.AddLine(fmt.Sprintf("\t(%s)", phonetic["text"]))
		}

		page.AddLine("")

		for j := 0; j < len(word.Meanings); j++ {
			meaning := word.Meanings[j]
			page.AddLine(meaning.PartOfSpeech)

			for k := 0; k < len(meaning.Definitions); k++ {
				definition := meaning.Definitions[k]
				page.AddH2Header("Definition:")
				page.AddLine(definition.Definition)
				if len(definition.Example) > 0 {
					page.AddH3Header("Example:")
					page.AddLine(definition.Example)
				}
				page.AddLine("")
			}
		}
	}
	return page.Build()
}

func handleLookup(w gemini.ResponseWriter, r *gemini.Request) {
	const URLTemplate string = "https://api.dictionaryapi.dev/api/v2/entries/en_US/%s"
	// TODO: Sanitize input
	word := r.URL.RawQuery
	log.Printf("Looking up \"%s\"\n", word)

	resp, err := http.Get(fmt.Sprintf(URLTemplate, word))
	if err != nil {
		log.Fatalln(err)
	}
	// Why is the body the stream?
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Fatalln(err)
	}

	w.Write(definitionToPage(body))
}

func handleRoot(w gemini.ResponseWriter, r *gemini.Request) {
	page := newAthenaDocument()
	page.AddLine("Search for a word to continue")
	w.Write(page.Build())
}

func main() {
	cfg := configure()
	lookupHandler := gemini.RequireInputHandler(gemini.HandlerFunc(handleLookup), "Enter seach word")
	rootHandler := gemini.HandlerFunc(handleRoot)

	router := mux.NewMux()
	router.AddRoute("/lookup", lookupHandler)
	router.AddRoute("/", rootHandler)

	ctx := context.Background()
	domainHandler, err := gemini.NewDomainHandlerFromFiles(cfg.domain, cfg.certPath, cfg.keyPath, router)
	if err != nil {
		log.Fatal("Error creating domain handler:", err)
	}

	if err != nil {
		log.Fatal("error creating domain handler B:", err)
	}

	// Start the server for two domains (a.gemini / b.gemini).
	addr := fmt.Sprintf(":%d", cfg.port)
	err = gemini.ListenAndServe(ctx, addr, domainHandler)
	if err != nil {
		log.Fatal("error:", err)
	}
}

A  => public/footer.gmi +7 -0
@@ 1,7 @@
---
Written and hosted by Andrew Thorp (andrew dot thorp dot dev at gmail dot com)
=> https://thorp.dev My personal website

Powered by Free Dictionary API.
Please support Free Dictionary API's server costs.
=> https://dictionaryapi.dev Free Dictionary API

A  => public/header.gmi +9 -0
@@ 1,9 @@
```
┏━┃━┏┛┃ ┃┏━┛┏━ ┏━┃
┏━┃ ┃ ┏━┃┏━┛┃ ┃┏━┃
┛ ┛ ┛ ┛ ┛━━┛┛ ┛┛ ┛ Dictionary
```
Welcome to Athena, the Open Source Gemini Dictionary powered by Free Dictionary API!

=> lookup/ Search for a word!