diff --git a/README.md b/README.md index 43245d62..5dc94c0f 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Welcome, fedinaut! localhost.localdomain:8443 is an instance of tootik, a federa 🛟 Help ``` +[![Latest release](https://img.shields.io/github/v/release/dimkr/tootik)](https://github.com/dimkr/tootik/releases) [![Build status](https://github.com/dimkr/tootik/actions/workflows/ci.yml/badge.svg)](https://github.com/dimkr/tootik/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/dimkr/tootik.svg)](https://pkg.go.dev/github.com/dimkr/tootik) + ## Overview tootik is a federated nanoblogging service for the small internet. With tootik, you can interact with your friends, including those on [Mastodon](https://joinmastodon.org/), [Lemmy](https://join-lemmy.org/) and other [ActivityPub](https://www.w3.org/TR/activitypub/)-compatible servers, from the comfort of a minimalistic, text-based interface in the small internet: @@ -98,34 +100,6 @@ or, to build a static executable: go build -tags netgo,sqlite_omit_load_extension,fts5 -ldflags "-linkmode external -extldflags -static" ./cmd/tootik -## Directory Structure - -* cmd/ implements main(). - -* outbox/ translates user actions into a queue of activities that need to be sent to other servers. -* inbox/ processes a queue of activities received from other servers. - * inbox/note/ handles insertion of posts: both posts received from other servers and posts created by local users. -* fed/ implements federation: it handles all communication with other servers, sends what's in the "outbox" queue to other servers and adds incoming activities to the "inbox" queue. - * fed/icon/ generates pseudo-random icons used as avatars. - -* front/ implements the frontend. - * front/static/ contains static content like the help page. - * text/text/plain/ converts HTML to plain text. - * text/text/gmi/ contains a gemtext writer. - * text/text/gmap/ contains a gophermap writer with line wrapping. - * text/text/guppy/ contains a Guppy response writer. - * front/gemini/ exposes the frontend over Gemini. - * front/gopher/ exposes the frontend over Gopher. - * front/finger/ exposes some content over Finger. - * front/guppy/ exposes the frontend over Guppy. - -* ap/ implements ActivityPub vocabulary. -* migrations/ contains the database schema. -* data/ contains useful data structures. -* cfg/ contains global configuration parameters. - -* test/ contains tests. - ## Gemini Frontend * /local shows a compact list of local posts; each entry contains a link to /view. diff --git a/ap/activity.go b/ap/activity.go index e7714ea9..a5a75d02 100644 --- a/ap/activity.go +++ b/ap/activity.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -45,6 +45,8 @@ type anyActivity struct { CC Audience `json:"cc"` } +// Activity represents an ActivityPub activity. +// Object can point to another Activity, an [Object] or a string. type Activity struct { Context any `json:"@context,omitempty"` ID string `json:"id"` diff --git a/ap/actor.go b/ap/actor.go index 4bc6aa64..fd8accef 100644 --- a/ap/actor.go +++ b/ap/actor.go @@ -23,6 +23,7 @@ const ( Group ActorType = "Group" ) +// Object represents an ActivityPub actor. type Actor struct { Context any `json:"@context"` ID string `json:"id"` diff --git a/ap/ap.go b/ap/ap.go new file mode 100644 index 00000000..81bd5dfe --- /dev/null +++ b/ap/ap.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package ap implements types that represent the ActivityPub vocabulary +package ap diff --git a/ap/audience.go b/ap/audience.go index 109e7ff9..af6e87df 100644 --- a/ap/audience.go +++ b/ap/audience.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import ( "github.com/dimkr/tootik/data" ) +// Audience is an ordered, unique list of actor IDs. type Audience struct { data.OrderedMap[string, struct{}] } diff --git a/ap/object.go b/ap/object.go index 2242f9a7..bac9dbfe 100644 --- a/ap/object.go +++ b/ap/object.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,6 +25,8 @@ const ( QuestionObject ObjectType = "Question" ) +// Object represents most ActivityPub objects. +// Actors are represented by Actor. type Object struct { Context any `json:"@context,omitempty"` ID string `json:"id"` diff --git a/ap/public.go b/ap/public.go index 6a54cd0b..47fe4431 100644 --- a/ap/public.go +++ b/ap/public.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,4 +16,5 @@ limitations under the License. package ap +// Public is the special ActivityPub collection used for public addressing. const Public = "https://www.w3.org/ns/activitystreams#Public" diff --git a/ap/time.go b/ap/time.go index 8ba2dcd0..b119a188 100644 --- a/ap/time.go +++ b/ap/time.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package ap import "time" +// Time is a wrapper around time.Time with fallback if parsing of RFC3339 fails type Time struct { time.Time } diff --git a/cfg/cfg.go b/cfg/cfg.go index 8b6b0e8b..0d864488 100644 --- a/cfg/cfg.go +++ b/cfg/cfg.go @@ -14,10 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package cfg defines the tootik configuration file format and defaults package cfg import "time" +// Config represents a tootik configuration file. type Config struct { RegistrationInterval time.Duration @@ -85,6 +87,7 @@ type Config struct { DeliveryTTL time.Duration } +// FillDefaults replaces missing or invalid settings with defaults. func (c *Config) FillDefaults() { if c.RegistrationInterval <= 0 { c.RegistrationInterval = time.Hour diff --git a/data/data.go b/data/data.go new file mode 100644 index 00000000..0ec25f7b --- /dev/null +++ b/data/data.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package data implements generic data types deletion of old data +package data diff --git a/data/garbage.go b/data/garbage.go index 5bb136c7..92a456ad 100644 --- a/data/garbage.go +++ b/data/garbage.go @@ -24,6 +24,7 @@ import ( "time" ) +// CollectGarbage deletes old data. func CollectGarbage(ctx context.Context, domain string, cfg *cfg.Config, db *sql.DB) error { now := time.Now() diff --git a/data/id.go b/data/id.go index 1faabd4a..9ca49e0e 100644 --- a/data/id.go +++ b/data/id.go @@ -21,6 +21,7 @@ import ( "strings" ) +// IsIDValid determines whether or not a string can be a valid actor, object or activity ID. func IsIDValid(id string) bool { if id == "" { return false diff --git a/data/map.go b/data/map.go index c716f1e7..348b7a0c 100644 --- a/data/map.go +++ b/data/map.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -21,19 +21,24 @@ type valueAndIndex[TV any] struct { index int } +// OrderedMap is a map that maintains insertion order. Listing of keys (using [OrderedMap.Keys]) iterates over keys and allocates memory. type OrderedMap[TK comparable, TV any] map[TK]valueAndIndex[TV] +// Contains determines if the map contains a key. func (m OrderedMap[TK, TV]) Contains(key TK) bool { _, contains := m[key] return contains } +// Store adds a key/value pair to the map if the map doesn't contain it already. func (m OrderedMap[TK, TV]) Store(key TK, value TV) { if _, dup := m[key]; !dup { m[key] = valueAndIndex[TV]{value, len(m)} } } +// Keys returns a list of keys in the map. +// To do so, it iterates over keys and allocates memory. func (m OrderedMap[TK, TV]) Keys() []TK { l := make([]TK, len(m)) @@ -44,6 +49,9 @@ func (m OrderedMap[TK, TV]) Keys() []TK { return l } +// Range iterates over the map and calls a callback for each key/value pair. +// Iteration stops if the callback returns false. +// Range calls [OrderedMap.Keys], therefore it allocates memory. func (m OrderedMap[TK, TV]) Range(f func(key TK, value TV) bool) { for _, k := range m.Keys() { if !f(k, m[k].value) { diff --git a/fed/blocklist.go b/fed/blocklist.go index cc2cd1d3..36e05b76 100644 --- a/fed/blocklist.go +++ b/fed/blocklist.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,6 +28,7 @@ import ( "time" ) +// BlockList is a list of blocked domains. type BlockList struct { lock sync.Mutex wg sync.WaitGroup @@ -131,6 +132,7 @@ func NewBlockList(log *slog.Logger, path string) (*BlockList, error) { return b, nil } +// Contains determines if a domain is blocked. func (b *BlockList) Contains(domain string) bool { b.lock.Lock() _, contains := b.domains[domain] @@ -138,6 +140,7 @@ func (b *BlockList) Contains(domain string) bool { return contains } +// Close frees resources. func (b *BlockList) Close() { b.w.Close() b.wg.Wait() diff --git a/fed/deliver.go b/fed/deliver.go index 180cd079..558c8d24 100644 --- a/fed/deliver.go +++ b/fed/deliver.go @@ -29,6 +29,7 @@ import ( "time" ) +// ProcessQueue polls the queue of outgoing activities and delivers them to other servers with timeout and retries. func ProcessQueue(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logger, db *sql.DB, resolver *Resolver) { t := time.NewTicker(cfg.OutboxPollingInterval) defer t.Stop() diff --git a/fed/fed.go b/fed/fed.go new file mode 100644 index 00000000..a0c532bb --- /dev/null +++ b/fed/fed.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package fed implements federation (server-to-server requests). +package fed diff --git a/fed/icon/icon.go b/fed/icon/icon.go index fcdce284..6d3ecd4f 100644 --- a/fed/icon/icon.go +++ b/fed/icon/icon.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package icon generates tiny, pseudo-random user avatars package icon import ( @@ -30,6 +31,7 @@ const ( FileNameExtension = ".gif" ) +// Generate generates a tiny pseudo-random image by user name func Generate(s string) ([]byte, error) { hash := sha256.Sum256([]byte(s)) diff --git a/fed/listener.go b/fed/listener.go index 0cf716a3..03b7985e 100644 --- a/fed/listener.go +++ b/fed/listener.go @@ -42,6 +42,7 @@ func robots(w http.ResponseWriter, r *http.Request) { w.Write([]byte("Disallow: /\n")) } +// ListenAndServe handles HTTP requests from other servers. func ListenAndServe(ctx context.Context, domain string, logLevel slog.Level, cfg *cfg.Config, db *sql.DB, resolver *Resolver, actor *ap.Actor, log *slog.Logger, addr, cert, key string, plain bool) error { mux := http.NewServeMux() mux.HandleFunc("/robots.txt", robots) diff --git a/fed/resolve.go b/fed/resolve.go index 1d7e608c..5f749d4a 100644 --- a/fed/resolve.go +++ b/fed/resolve.go @@ -46,12 +46,14 @@ type webFingerResponse struct { } `json:"links"` } +// Resolver retrieves actor objects given their ID. +// Actors are cached, updated periodically and deleted if gone from the remote server. type Resolver struct { - Client http.Client BlockedDomains *BlockList Domain string Config *cfg.Config locks []*semaphore.Weighted + client http.Client } var ( @@ -61,22 +63,23 @@ var ( ErrInvalidScheme = errors.New("invalid scheme") ) +// NewResolver returns a new [Resolver]. func NewResolver(blockedDomains *BlockList, domain string, cfg *cfg.Config) *Resolver { transport := http.Transport{ MaxIdleConns: cfg.ResolverMaxIdleConns, IdleConnTimeout: cfg.ResolverIdleConnTimeout, } r := Resolver{ - Client: http.Client{ + BlockedDomains: blockedDomains, + Domain: domain, + Config: cfg, + locks: make([]*semaphore.Weighted, cfg.MaxResolverRequests), + client: http.Client{ Transport: &transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }, }, - BlockedDomains: blockedDomains, - Domain: domain, - Config: cfg, - locks: make([]*semaphore.Weighted, cfg.MaxResolverRequests), } for i := 0; i < len(r.locks); i++ { r.locks[i] = semaphore.NewWeighted(1) @@ -85,6 +88,7 @@ func NewResolver(blockedDomains *BlockList, domain string, cfg *cfg.Config) *Res return &r } +// Resolve retrieves an actor object. func (r *Resolver) Resolve(ctx context.Context, log *slog.Logger, db *sql.DB, from *ap.Actor, to string, offline bool) (*ap.Actor, error) { u, err := url.Parse(to) if err != nil { diff --git a/fed/send.go b/fed/send.go index 8c632992..715bcbf8 100644 --- a/fed/send.go +++ b/fed/send.go @@ -104,7 +104,7 @@ func (r *Resolver) send(log *slog.Logger, db *sql.DB, from *ap.Actor, req *http. } } - resp, err := r.Client.Do(req) + resp, err := r.client.Do(req) if err != nil { return nil, fmt.Errorf("failed to send request to %s: %w", urlString, err) } @@ -121,6 +121,7 @@ func (r *Resolver) send(log *slog.Logger, db *sql.DB, from *ap.Actor, req *http. return resp, nil } +// Send sends a signed request. func (r *Resolver) Send(ctx context.Context, log *slog.Logger, db *sql.DB, from *ap.Actor, inbox string, body []byte) error { if inbox == "" { return fmt.Errorf("cannot send request to %s: empty URL", inbox) diff --git a/front/finger/finger.go b/front/finger/finger.go index 5d9615a1..fe09417e 100644 --- a/front/finger/finger.go +++ b/front/finger/finger.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package finger exposes a limited Finger interface. package finger import ( @@ -179,6 +180,7 @@ func handle(ctx context.Context, domain string, cfg *cfg.Config, conn net.Conn, } } +// ListenAndServe handles Finger queries. func ListenAndServe(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logger, db *sql.DB, addr string) error { l, err := net.Listen("tcp", addr) if err != nil { diff --git a/front/front.go b/front/front.go new file mode 100644 index 00000000..c86246f2 --- /dev/null +++ b/front/front.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package front implements a UI and exposes it over text-based protocols +package front diff --git a/front/gemini/gemini.go b/front/gemini/gemini.go index a7c23f05..7a186cb6 100644 --- a/front/gemini/gemini.go +++ b/front/gemini/gemini.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package gemini exposes a Gemini interface. package gemini import ( @@ -65,6 +66,7 @@ func getUser(ctx context.Context, domain string, db *sql.DB, conn net.Conn, tlsC return &actor, nil } +// Handle handles a Gemini request. func Handle(ctx context.Context, domain string, cfg *cfg.Config, handler front.Handler, conn net.Conn, db *sql.DB, resolver *fed.Resolver, wg *sync.WaitGroup, log *slog.Logger) { if err := conn.SetDeadline(time.Now().Add(cfg.GeminiRequestTimeout)); err != nil { log.Warn("Failed to set deadline", "error", err) @@ -128,6 +130,7 @@ func Handle(ctx context.Context, domain string, cfg *cfg.Config, handler front.H handler.Handle(ctx, log, w, reqUrl, user, db, resolver, wg) } +// ListenAndServe handles Gemini requests. func ListenAndServe(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logger, db *sql.DB, handler front.Handler, resolver *fed.Resolver, addr, certPath, keyPath string) error { cert, err := tls.LoadX509KeyPair(certPath, keyPath) if err != nil { diff --git a/front/gopher/gopher.go b/front/gopher/gopher.go index ff7cb99e..ab4f4289 100644 --- a/front/gopher/gopher.go +++ b/front/gopher/gopher.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package gopher exposes a limited Gopher interface. package gopher import ( @@ -76,6 +77,7 @@ func handle(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logge handler.Handle(ctx, log.With(slog.Group("request", "path", reqUrl.Path)), w, reqUrl, nil, db, resolver, wg) } +// ListenAndServe handles Gopher requests. func ListenAndServe(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logger, handler front.Handler, db *sql.DB, resolver *fed.Resolver, addr string) error { l, err := net.Listen("tcp", addr) if err != nil { diff --git a/front/graph/bars.go b/front/graph/bars.go index 6e169da2..c2405e3a 100644 --- a/front/graph/bars.go +++ b/front/graph/bars.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package graph draws text-based graphs. package graph import ( @@ -22,6 +23,7 @@ import ( "unicode/utf8" ) +// Bars builds a bar graph. func Bars(keys []string, values []int64) string { flip := false var keyWidth int diff --git a/front/guppy/guppy.go b/front/guppy/guppy.go index b773cee9..8424d460 100644 --- a/front/guppy/guppy.go +++ b/front/guppy/guppy.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package guppy exposes a limited Guppy interface. package guppy import ( @@ -197,6 +198,7 @@ func handle(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logge } } +// ListenAndServe handles Guppy requests. func ListenAndServe(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logger, db *sql.DB, handler front.Handler, resolver *fed.Resolver, addr string) error { l, err := net.ListenPacket("udp", addr) if err != nil { diff --git a/front/handler.go b/front/handler.go index edf72627..a44f0ae6 100644 --- a/front/handler.go +++ b/front/handler.go @@ -33,6 +33,7 @@ import ( "time" ) +// Handler handles frontend (client-to-server) requests. type Handler struct { handlers map[*regexp.Regexp]func(text.Writer, *request, ...string) Domain string @@ -49,6 +50,7 @@ func serveStaticFile(w text.Writer, r *request, args ...string) { } } +// NewHandler returns a new [Handler]. func NewHandler(domain string, closed bool, cfg *cfg.Config) Handler { h := Handler{ handlers: map[*regexp.Regexp]func(text.Writer, *request, ...string){}, @@ -133,6 +135,7 @@ func NewHandler(domain string, closed bool, cfg *cfg.Config) Handler { return h } +// Handle handles a request and writes a response. func (h *Handler) Handle(ctx context.Context, log *slog.Logger, w text.Writer, reqUrl *url.URL, user *ap.Actor, db *sql.DB, resolver *fed.Resolver, wg *sync.WaitGroup) { for re, handler := range h.handlers { m := re.FindStringSubmatch(reqUrl.Path) @@ -156,7 +159,8 @@ func (h *Handler) Handle(ctx context.Context, log *slog.Logger, w text.Writer, r WaitGroup: wg, Log: l, }, - m...) + m..., + ) return } } diff --git a/front/static/embed.go b/front/static/embed.go index 4fbb566c..d14c6826 100644 --- a/front/static/embed.go +++ b/front/static/embed.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package static serves static content. package static import ( @@ -25,6 +26,7 @@ import ( //go:embed *.gmi */*.gmi var rawFiles embed.FS +// Files maps relative paths to static content. var Files = map[string][]string{} func readDirectory(dir string) { diff --git a/front/text/base.go b/front/text/base.go index 487ad431..5e016dec 100644 --- a/front/text/base.go +++ b/front/text/base.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package text import "io" +// Base is a base for implementations of [Writer]. type Base struct { io.Writer } diff --git a/front/text/gmap/writer.go b/front/text/gmap/writer.go index 11a537f6..a1d37b7b 100644 --- a/front/text/gmap/writer.go +++ b/front/text/gmap/writer.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package gmap builds gophermaps. package gmap import ( @@ -31,6 +32,7 @@ type writer struct { Config *cfg.Config } +// Wrap wraps an [io.Writer] with a gophermap writer. func Wrap(w io.Writer, domain string, cfg *cfg.Config) text.Writer { return &writer{Base: text.Base{Writer: w}, Domain: domain, Config: cfg} } diff --git a/front/text/gmi/writer.go b/front/text/gmi/writer.go index 8ac56de5..a8421100 100644 --- a/front/text/gmi/writer.go +++ b/front/text/gmi/writer.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package gmi build Gemini responses. package gmi import ( @@ -26,6 +27,7 @@ type writer struct { text.Base } +// Wrap wraps an [io.Writer] with a Gemini response writer. func Wrap(w io.Writer) text.Writer { return &writer{Base: text.Base{Writer: w}} } diff --git a/front/text/guppy/writer.go b/front/text/guppy/writer.go index 0a90aba5..a3c51142 100644 --- a/front/text/guppy/writer.go +++ b/front/text/guppy/writer.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package guppy builds Guppy responses. package guppy import ( @@ -28,6 +29,7 @@ type Writer struct { seq int } +// Wrap wraps an [io.Writer] with a Guppy response writer. func Wrap(w io.Writer, seq int) *Writer { return &Writer{Writer: gmi.Wrap(w), seq: seq} } diff --git a/front/text/plain/convert.go b/front/text/plain/convert.go index 1f6b5dd0..e0cb8585 100644 --- a/front/text/plain/convert.go +++ b/front/text/plain/convert.go @@ -42,6 +42,7 @@ var ( mentionRegex = regexp.MustCompile(`\B@(\w+)(?:@(?:(?:\w+\.)+\w+(?::\d{1,5}){0,1})){0,1}\b`) ) +// FromHTML converts HTML to plain text and extracts links. func FromHTML(text string) (string, data.OrderedMap[string, string]) { res := html.UnescapeString(text) links := data.OrderedMap[string, string]{} @@ -117,6 +118,7 @@ func FromHTML(text string) (string, data.OrderedMap[string, string]) { return strings.TrimRight(res, " \n\r\t"), links } +// ToHTML converts plain text to HTML. func ToHTML(text string, mentions []ap.Mention) string { if text == "" { return "" diff --git a/front/text/plain/plain.go b/front/text/plain/plain.go new file mode 100644 index 00000000..2f79fc4e --- /dev/null +++ b/front/text/plain/plain.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package plain converts HTML to plain text and vice versa. +package plain diff --git a/front/text/text.go b/front/text/text.go new file mode 100644 index 00000000..77acc804 --- /dev/null +++ b/front/text/text.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package text is an abstraction layer for different text formats. +package text diff --git a/front/text/wrap.go b/front/text/wrap.go index 6d16a161..d5d310eb 100644 --- a/front/text/wrap.go +++ b/front/text/wrap.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ limitations under the License. package text +// WordWrap wraps long lines. func WordWrap(text string, width, maxLines int) []string { if text == "" { return []string{""} diff --git a/front/text/writer.go b/front/text/writer.go index ce7bf167..297c9df1 100644 --- a/front/text/writer.go +++ b/front/text/writer.go @@ -1,5 +1,5 @@ /* -Copyright 2023 Dima Krasner +Copyright 2023, 2024 Dima Krasner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package text import "io" +// Writer builds a textual response. type Writer interface { io.Writer diff --git a/front/user/create.go b/front/user/create.go index 8961ea75..a177d476 100644 --- a/front/user/create.go +++ b/front/user/create.go @@ -62,6 +62,7 @@ func gen(ctx context.Context) ([]byte, []byte, error) { return privPem.Bytes(), pubPem.Bytes(), nil } +// Create creates a new user. func Create(ctx context.Context, domain string, db *sql.DB, name, certHash string) (*ap.Actor, error) { priv, pub, err := gen(ctx) if err != nil { diff --git a/front/user/nobody.go b/front/user/nobody.go index da6eb909..2893e42a 100644 --- a/front/user/nobody.go +++ b/front/user/nobody.go @@ -25,6 +25,8 @@ import ( "github.com/dimkr/tootik/ap" ) +// CreateNobody creates the special "nobdoy" user. +// This user is used to sign outgoing requests not initiated by a particular user. func CreateNobody(ctx context.Context, domain string, db *sql.DB) (*ap.Actor, error) { var actorString string if err := db.QueryRowContext(ctx, `select actor from persons where actor->>'preferredUsername' = 'nobody' and host = ?`, domain).Scan(&actorString); err != nil && !errors.Is(err, sql.ErrNoRows) { diff --git a/front/user/user.go b/front/user/user.go new file mode 100644 index 00000000..491f147f --- /dev/null +++ b/front/user/user.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package user handles user registration. +package user diff --git a/inbox/inbox.go b/inbox/inbox.go new file mode 100644 index 00000000..81c283b9 --- /dev/null +++ b/inbox/inbox.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package inbox processes activities received from other servers +package inbox diff --git a/inbox/note/insert.go b/inbox/note/insert.go index 21546f4e..d4b9801c 100644 --- a/inbox/note/insert.go +++ b/inbox/note/insert.go @@ -14,6 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package note handles insertion of posts. package note import ( @@ -47,6 +48,7 @@ func expand(aud ap.Audience, arr *[3]sql.NullString) { } } +// Flatten converts a post into text that can be indexed for search purposes. func Flatten(note *ap.Object) string { content, links := plain.FromHTML(note.Content) if len(links) > 0 { @@ -71,6 +73,7 @@ func Flatten(note *ap.Object) string { return content } +// Insert inserts a post. func Insert(ctx context.Context, log *slog.Logger, tx *sql.Tx, note *ap.Object) error { body, err := json.Marshal(note) if err != nil { diff --git a/inbox/queue.go b/inbox/queue.go index ecc75cbf..4371a666 100644 --- a/inbox/queue.go +++ b/inbox/queue.go @@ -430,6 +430,7 @@ func processActivityWithTimeout(parent context.Context, domain string, cfg *cfg. } } +// ProcessBatch processes one batch of incoming activites in the queue. func ProcessBatch(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logger, db *sql.DB, resolver *fed.Resolver, from *ap.Actor) (int, error) { log.Debug("Polling activities queue") @@ -516,6 +517,7 @@ func processQueue(ctx context.Context, domain string, cfg *cfg.Config, log *slog } } +// ProcessQueue polls the queue of incoming activities and processes them. func ProcessQueue(ctx context.Context, domain string, cfg *cfg.Config, log *slog.Logger, db *sql.DB, resolver *fed.Resolver, from *ap.Actor) error { t := time.NewTicker(cfg.ActivitiesPollingInterval) defer t.Stop() diff --git a/migrations/000_initial.go b/migrations/000_initial.go index a0fdbd0b..83a21554 100644 --- a/migrations/000_initial.go +++ b/migrations/000_initial.go @@ -1,18 +1,3 @@ -/* -Copyright 2023, 2024 Dima Krasner - -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. -*/ package migrations import ( diff --git a/migrations/list.sh b/migrations/list.sh index ad0c50c4..a1b847a0 100755 --- a/migrations/list.sh +++ b/migrations/list.sh @@ -15,9 +15,10 @@ # limitations under the License. cat << EOF > migrations.go -// auto-generated by list.sh package migrations +// auto-generated by list.sh + var migrations = []migration{ EOF diff --git a/migrations/migration.go b/migrations/migration.go index 0bce0516..ad30a65d 100644 --- a/migrations/migration.go +++ b/migrations/migration.go @@ -14,6 +14,11 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package migrations defines the database schema. +// +// migrations.go is generated by go generate and lists migrations to run. +// +// To add a new, empty migration, run add.sh. package migrations import ( @@ -53,6 +58,7 @@ func applyMigration(ctx context.Context, domain string, db *sql.DB, m migration) return nil } +// Run runs all migrations. func Run(ctx context.Context, log *slog.Logger, domain string, db *sql.DB) error { if _, err := db.ExecContext(ctx, `create table if not exists migrations(id string not null primary key, applied integer default (unixepoch()))`); err != nil { return err diff --git a/outbox/outbox.go b/outbox/outbox.go new file mode 100644 index 00000000..d4c3c42d --- /dev/null +++ b/outbox/outbox.go @@ -0,0 +1,18 @@ +/* +Copyright 2024 Dima Krasner + +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. +*/ + +// Package outbox handles user actions and translates them into outgoing activities +package outbox