Skip to content

Commit

Permalink
document packages and public stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
dimkr committed Jan 17, 2024
1 parent 6dc72f2 commit 2f3f88e
Show file tree
Hide file tree
Showing 47 changed files with 263 additions and 66 deletions.
30 changes: 2 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion ap/activity.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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"`
Expand Down
1 change: 1 addition & 0 deletions ap/actor.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const (
Group ActorType = "Group"
)

// Object represents an ActivityPub actor.
type Actor struct {
Context any `json:"@context"`
ID string `json:"id"`
Expand Down
18 changes: 18 additions & 0 deletions ap/ap.go
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion ap/audience.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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{}]
}
Expand Down
4 changes: 3 additions & 1 deletion ap/object.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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"`
Expand Down
3 changes: 2 additions & 1 deletion ap/public.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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"
3 changes: 2 additions & 1 deletion ap/time.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
}
Expand Down
3 changes: 3 additions & 0 deletions cfg/cfg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions data/data.go
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions data/garbage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
1 change: 1 addition & 0 deletions data/id.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion data/map.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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))

Expand All @@ -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) {
Expand Down
5 changes: 4 additions & 1 deletion fed/blocklist.go
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -28,6 +28,7 @@ import (
"time"
)

// BlockList is a list of blocked domains.
type BlockList struct {
lock sync.Mutex
wg sync.WaitGroup
Expand Down Expand Up @@ -131,13 +132,15 @@ 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]
b.lock.Unlock()
return contains
}

// Close frees resources.
func (b *BlockList) Close() {
b.w.Close()
b.wg.Wait()
Expand Down
1 change: 1 addition & 0 deletions fed/deliver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
18 changes: 18 additions & 0 deletions fed/fed.go
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion fed/icon/icon.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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 (
Expand All @@ -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))

Expand Down
1 change: 1 addition & 0 deletions fed/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
16 changes: 10 additions & 6 deletions fed/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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)
Expand All @@ -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 {
Expand Down
Loading

0 comments on commit 2f3f88e

Please sign in to comment.