nuke everything, start over

This commit is contained in:
mStar 2024-05-27 18:10:35 +02:00
parent e261de7060
commit a2a937791d
44 changed files with 7 additions and 1106 deletions

View file

@ -1,13 +0,0 @@
FROM mcr.microsoft.com/devcontainers/go:1-1.21-bullseye
# [Optional] Uncomment this section to install additional OS packages.
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends postgresql-client
# [Optional] Uncomment the next lines to use go get to install anything else you need
# USER vscode
# RUN go get -x <your-dependency-or-tool>
# USER root
# [Optional] Uncomment this line to install global node packages.
# RUN su vscode -c "source /usr/local/share/nvm/nvm.sh && npm install -g <your-package-here>" 2>&1

View file

@ -1,23 +0,0 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/go-postgres
{
"name": "Go & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}"
// Features to add to the dev container. More info: https://containers.dev/features.
// "features": {},
// Configure tool-specific properties.
// "customizations": {},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [5432],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "go version",
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}

View file

@ -1,38 +0,0 @@
version: '3.8'
volumes:
postgres-data:
services:
app:
build:
context: .
dockerfile: Dockerfile
env_file:
# Ensure that the variables in .env match the same variables in devcontainer.json
- .env
volumes:
- ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
env_file:
# Ensure that the variables in .env match the same variables in devcontainer.json
- .env
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)

File diff suppressed because one or more lines are too long

View file

@ -1,25 +0,0 @@
# Feature targets
## UI
- Firefish like
- htmx
## Features
### QoL
- [ ] Reactions
- [ ] Qoute tweets
- [ ] misskey/firefish markdown
### Moderation
- [ ] Authorized Fetch
- [ ] Lockdown (user/server) (block/ignore any incoming messages)
- [ ] Filter-based (maybe regex) auto-block/refuse follow requests
### General
- [ ] Masto, MK/FF, Akoma/Plemora API
- [ ] is_cat

View file

@ -1,13 +0,0 @@
# Linstrom
An ActivityPub enabled social media server
## Goals
- A server for interacting with other ActivityPub enabled servers, also known as the fediverse
- Provide powerful, yet easy to use moderation tools for both admins and users to support the effort of keeping the users safe
## Todos
Todos can be found on [https://planka.evilthings.de]. Since Planka has no anonymous access as far as I can tell, there's an account for readonly access.
Details:
- Username: `public`
- Password: `Public user`

View file

@ -1,12 +0,0 @@
package ap
import "net/url"
// Sent when a follow request is approved, manually or automatically
type AcceptAction struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
Id url.URL `json:"id"` // Url to this action
Type string `json:"type"` // Should always be "Accept"
Actor url.URL `json:"actor"` // Who is accepting what
Object FollowAction `json:"object"` // Action that is being accepted
}

View file

@ -1,14 +0,0 @@
package ap
import "net/url"
// Announce == boost/share/retweet
type AnnounceAction struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
Id url.URL `json:"id"` // Url to this action
Type string `json:"type"` // Should always be "Announce"
Actor url.URL `json:"actor"` // Who is announcing something
To []url.URL `json:"to"` // Targets to share to
Cc []url.URL `json:"cc"` // More targets to send to
Object any `json:"object"` // Url to announced object. Url or embedded object, usually a note or survey
}

View file

@ -1,8 +0,0 @@
package ap
// An info field on an account page
type PersonInfoField struct {
Type string `json:"type"` // Type of the attachment. Probably should always be "PropertyValue"
Name string `json:"name"` // Name of the property. Example: Source
Value string `json:"value"` // Value of the property. Link or html or just some text. Example: https://gitlab.com/mstarongitlab/Linstrom
}

View file

@ -1,19 +0,0 @@
package ap
import (
"net/url"
"time"
)
// Sent when posting something
type CreateAction struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
ID url.URL `json:"id"` // URL to this resource
Actor url.URL `json:"actor"` // Account that created this action, links to the AP url
Type string `json:"type"` // Should always be "Create"
Published time.Time `json:"published"` // When this action was created
Object any `json:"object"` // Content of the action, is an AP object, like notes or questions (survey)
To []url.URL `json:"to"` // Array of url targets to deliver it to. With dms, oonly includes target accounts. Followers only goes to followers url of posting account
Cc []url.URL `json:"cc"` // More targets to deliver to?
Signature Signature `json:"signature"` // Signature of the message
}

View file

@ -1,15 +0,0 @@
package ap
import (
"net/url"
)
// When a message is deleted
type DeleteAction struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
Id url.URL `json:"id"` // Location of this resource
Type string `json:"type"` // Should always be "Delete"
Actor url.URL `json:"actor"` // Account deleting something
Target any `json:"object"` // What is being deleted, url or some object
Signature any `json:"signature"` // Probably useless
}

View file

@ -1,15 +0,0 @@
package ap
import (
"net/url"
"time"
)
// Custom emote information
type Emote struct {
Icon Media `json:"icon"` // Emote source file
ID url.URL `json:"id"` // Url of the image
Name string `json:"name"` // Name of the emote, example: ":neocat_heart:"
Type string `json:"type"` // Should always be "Emoji"
Updated time.Time `json:"updated"` // When this was last updated. Akkoma seems to set this to unix epoch
}

View file

@ -1,12 +0,0 @@
package ap
import "net/url"
// A follow request
type FollowAction struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
Id url.URL `json:"id"` // Location of this resource
Type string `json:"type"` // Should always be "Follow"
Actor url.URL `json:"actor"` // Account instanciating the follow request
Target url.URL `json:"object"` // Account that the actor wants to follow
}

View file

@ -1,17 +0,0 @@
package ap
import "net/url"
// Action to like a post
type LikeAction struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
Id url.URL `json:"id"` // Url to this action
Type string `json:"type"` // Should always be "Like"
Actor url.URL `json:"actor"` // Who is liking something
Object url.URL `json:"object"` // What is being liked
// Reaction data Misskey style
Content *string `json:"content,omitempty"` // Raw emote name
MkReaction *string `json:"_misskey_reaction,omitempty"` // Misskey version of content
Tag []Emote `json:"tag,omitempty"` // Emote in use
}

View file

@ -1,10 +0,0 @@
package ap
import "net/url"
// Media content (video files, images, audio)
type Media struct {
Type string `json:"type"` // Should probably always be "Image", "Video" or similar. Dunno if "Video" is correct
MediaType *string `json:"mediaType,omitempty"` // What media type the linked resource is. Something like "image/png"
Url url.URL `json:"url"` // Where to find the media file
}

View file

@ -1,10 +0,0 @@
package ap
import "net/url"
// Tag type for mentions
type Mention struct {
Type string `json:"type"` // Should always be "Mention"
Href url.URL `json:"href"` // Url to mentioned account
Name string `json:"name"` // Name of the account ("@foo@example.com")
}

View file

@ -1,30 +0,0 @@
package ap
import (
"net/url"
"time"
)
// A post
type Note struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if note is embedded as object in another object
ID url.URL `json:"id"` // Url to this resource
Type string `json:"type"` // Should always be "Note"
AttributedTo url.URL `json:"attributedTo"` // Author of this note
Content string `json:"content"` // Content of this note
Source *RawNoteContent `json:"source"` // Raw content
InReplyTo *url.URL `json:"inReplyTo,omitempty"` // Note this is a reply to. Empty if not a reply
Attachment []map[string]string `json:"attachment"` // Media attachments - TODO: Make proper type for this
Tag []any `json:"tag"` // List of hashtags, mentions & custom emotes
Published time.Time `json:"published"` // When this note was created
To []url.URL `json:"to"` // Where this note should get sent to
Cc []url.URL `json:"cc"` // More where this note should get sent to
Summary *string `json:"summary"` // Summary of a post (also known as content warning)
Sensitive bool `json:"sensitive"` // Does this note have a content warning?
MkContent *string `json:"_misskey_content,omitempty"` // Misskey version of content
MkQuote *url.URL `json:"_misskey_quote,omitempty"` // Misskey link to note this quotes
QuoteUrl *url.URL `json:"quoteUrl,omitempty"` // Link to quoted note object
QuoteUri *url.URL `json:"quoteUri,omitempty"` // Same as Quote Url?
}

View file

@ -1,25 +0,0 @@
package ap
import "net/url"
// Ordered collection of things (paged)
type OrderedCollection struct {
Context map[string]any `json:"@context"` // Big chunk of hopefully don't give a fuck
ID url.URL `json:"id"` // URL to this resource
Type string `json:"type"` // Should always be "OrderedCollection"
TotalItems *int `json:"totalItems,omitempty"` // Number of resources in this collection, Akoma doesn't include this
First url.URL `json:"first"` // Link to the first resource in this collection, an OrderedCollectionPage object
Last url.URL `json:"last"` // Link to the last resource in this collection, an OrderedCollectionPage object
}
// One page of an ordered collection
type OrderedCollectionPage struct {
Context map[string]any `json:"@context"` // Big chunk of hopefully don't give a fuck
ID url.URL `json:"id"` // URL to this resource
PartOf url.URL `json:"partOf"` // URL to the collection this is a part of
Type string `json:"type"` // Should always be "OrderedCollectionPage"
TotalItems *int `json:"totalItems,omitempty"` // Number of resources in this collection. Mk yes, Masto no, Akoma no
OrderedItems []map[string]any `json:"orderedItems"` // Items in this list. Should all be AP objects/actions
Previous *url.URL `json:"prev"` // Previous page, empty if no items? I think?
Next *url.URL `json:"next"` // Next page
}

View file

@ -1,51 +0,0 @@
package ap
import (
"net/url"
"time"
)
// An account
type Person struct {
Context map[string]any `json:"@context"` // Big chunk of hopefully don't give a fuck
ID url.URL `json:"id"` // URL to this resource
Type string `json:"type"` // Should always be of content "Person" (or "Service", if a bot)
Following url.URL `json:"following"` // Ordered collection of accounts this account follows
Followers url.URL `json:"followers"` // Ordered collection of accounts following this account
Inbox url.URL `json:"inbox"` // Where others send activities to (posts, follow requests, reactions, etc)
Outbox url.URL `json:"outbox"` // Where others can read this account's activities from
Featured url.URL `json:"featured"` // Ordered collection of something featured - TODO: Find out what this is
PreferredUsername string `json:"preferredUsername"` // Username
Name string `json:"name"` // Vanity name
Summary string `json:"summary"` // Description of the account. Could contain html or maybe mfm for *key
Url url.URL `json:"url"` // Public location of the account
ManualApproval bool `json:"manuallyApprovesFollowers"` // Does this account have to approve of follows before they are actual follows?
Discoverable bool `json:"discoverable"` // If this account can be found via search things? I guess?
PublicKey PublicKey `json:"publicKey"` // public key of that account. For verifying that that account is actually them
Tag []Emote `json:"tag"` // Contains custom emote info - TODO: Add proper type
Attachment PersonInfoField `json:"attachment"` // Additional profile information (fields below the description, like "Source: https://gitlab.com/mstarongitlab/linstrom")
Endpoints map[string]string `json:"endpoints"` // Stores at least the shared inbox of the server this account is on in "sharedInbox", which is an url
Icon *Media `json:"icon"` // Profile image of the account, null if not set
Image *Media `json:"image"` // Header image of the account, null if not set
// Section: Mastodon only
Indexable *bool `json:"indexable,omitempty"` // Probably if this account will be shown to search engines and crawlers?
FeaturedTags *url.URL `json:"featuredTags,omitempty"` // Collection of something, not ordered - TODO: Find out what this is
Published *time.Time `json:"published,omitempty"` // When this account was created I guess?
Memorial *bool `json:"memorial,omitempty"` // If this account is closed? Or moved? Probably closed
Devices *url.URL `json:"devices,omitempty"` // Collection of something - TODO: Find out what this is
// Section: Misskey only
SharedInbox *url.URL `json:"sharedInbox,omitempty"` // Replicates the shared inbox url here I guess
MkSummary *string `json:"_misskey_summary,omitempty"` // Misskey's version of the summary
Background *url.URL `json:"backgroundUrl,omitempty"` // Background image
IsCat *bool `json:"isCat,omitempty"` // Is the user a cat (apply cat ears in that case)
NoIndex *bool `json:"noindex,omitempty"` // probably the same as Indexable, just reversed?
SpeakAsCat *bool `json:"speakAsCat,omitempty"` // Does the account speak like a cat
Birthday *string `json:"vcard:bday,omitempty"` // Birthday date, but doesn't quite follow normalised datetime
Location *string `json:"vcard:Address,omitempty"` // Where the user of the account lives, could technically be anything as just a string
// Section: Akkoma things
AlsoKnownAs *[]any `json:"alsoKnownAs"` // Meaning unknown
Capabilities *map[string]any `json:"capabilities"` // Meaning unknown
}

View file

@ -1,22 +0,0 @@
package ap
import "net/url"
// The public key associated with an account
type PublicKey struct {
ID url.URL `json:"id"` // ID of the key
Owner url.URL `json:"owner"` // Who this key belongs to
Content string `json:"publicKeyPem"` // Actual content of the key, example below
}
/*
Example for a public key
"""
-----BEGIN PUBLIC KEY-----
Big blob of random characters
-----END PUBLIC KEY-----
"""
*/

View file

@ -1,11 +0,0 @@
package ap
// One option for a survey
type QuestionEntry struct {
Type string `json:"type"` // Should always be "Note"
Name string `json:"name"` // Name of the entry
Replies struct {
Type string `json:"type"` // Should always be "Collection"
TotalItems int // Nr of people that selected this
} `json:"replies"` // Nr of people that selected this option
}

View file

@ -1,28 +0,0 @@
package ap
import (
"net/url"
"time"
)
// A survey. Options are in the field OneOf if it's single choice, AnyOf if multipleChoice
type Question struct {
Context map[string]any `json:"@context,omitempty"` // Lots of something. Not included if note is embedded as object in another object
ID url.URL `json:"id"` // Url to this object
Type string `json:"type"` // Should always be "Question"
AttributedTo url.URL `json:"attributedTo"` // Creator of this object
Content string `json:"content"` // Preformated text content
MkContent string `json:"_misskey_content"` // Misskey version of the content
Source RawNoteContent `json:"source"` // Raw version of the content
Published time.Time `json:"published"` // When this object was published
To []url.URL `json:"to"` // Who to send this to
Cc []url.URL `json:"cc"` // More targets to send to
InReplyTo *url.URL `json:"inReplyTo"` // Object this post replies to
Attachment []map[string]string `json:"attachment"` // File attachments - TODO: Move to concrete type
Sensitive bool `json:"sensitive"` // Whether this post has a content warning
Summary *string `json:"summary"` // Summary of a post (also known as content warning)
Tag []Hashtag `json:"tag"` // Hashtags
EndTime *time.Time `json:"endTime"` // Timestamp of when the question ends
OneOf []QuestionEntry `json:"oneOf,omitempty"` // All available options to vote for. Used if single choice vote
AnyOf []QuestionEntry `json:"anyOf,omitempty"` // All available options to vote for. Used if multiple choice vote
}

View file

@ -1,7 +0,0 @@
package ap
// Raw content of a note I think. Added by Misskey and Akkoma
type RawNoteContent struct {
Content string `json:"content"`
MediaType string `json:"mediaType"`
}

View file

@ -1,12 +0,0 @@
package ap
import "net/url"
// Follow request rejected (or maybe I think "confirmation" on unfollow)
type RejectAction struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
Id url.URL `json:"id"` // Location of this resource
Type string `json:"type"` // Should always be "Reject"
Actor url.URL `json:"actor"` // Account rejecting the follow request
Request FollowAction `json:"object"` // Request that's being rejected
}

View file

@ -1,13 +0,0 @@
package ap
import (
"net/url"
"time"
)
type Signature struct {
Created time.Time `json:"created"` // When this signature was created
Creator url.URL `json:"creator"` // What key was used to create it
Value string `json:"signatureValue"` // The signature itself
Type string `json:"type"` // What algorithm was used
}

View file

@ -1,10 +0,0 @@
package ap
import "net/url"
// Describes a hashtag's data
type Hashtag struct {
Type string `json:"type"` // Should always be "Hashtag"
Href url.URL `json:"href"` // Url to that hashtag
Name string `json:"name"` // Name of the hashtag, example: "#some-hashtag"
}

View file

@ -1,10 +0,0 @@
package ap
import "net/url"
// The tombstone of a message
type Tombstone struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
Id url.URL `json:"id"` // Location of this resource
Type string `json:"type"` // Should always be "Tombstone"
}

View file

@ -1,12 +0,0 @@
package ap
import "net/url"
// Undoes one action
type UndoAction struct {
Context *map[string]any `json:"@context,omitempty"` // Lots of something. Not included if action is embedded as object in another object
Id url.URL `json:"id"` // Location of this resource
Type string `json:"type"` // Should always be "Undo"
Actor url.URL `json:"actor"` // Account instanciating the follow request
Target any `json:"object"` // Action to undo, usually a like or follow
}

View file

@ -1,4 +0,0 @@
[General]
database_path="postgres://postgres:postgres@localhost/postgres"
handle_ssl=true
enable_ui=true

View file

@ -1,87 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package config
import (
"flag"
"os"
"github.com/BurntSushi/toml"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
log "github.com/sirupsen/logrus"
)
type CLIArguments struct {
// Path to a config file
ConfigFile string
// Path to a postgres database
// Nil if not provided
DbPath *string
}
type Config struct {
General struct {
// Path to the postgres db
DbPath string `envconfig:"DATABASE_PATH" toml:"database_path"`
// Whether the server should handle ssl itself
HandleSSL bool `envconfig:"HANDLE_SSL" toml:"handle_ssl"`
// Whether to enable the builtin frontend
EnableUI bool `envconfig:"ENABLE_UI" toml:"enable_ui"`
Domain string `envconfig:"DOMAIN" toml:"domain"`
}
}
// Reads arguments passed to the application
func ReadCLIArguments() CLIArguments {
configFlag := flag.String("config", "config.toml", "Path to a config file")
dbFlag := flag.String("db", "", "Path to a postgres database")
flag.Parse()
var dbString *string
if *dbFlag != "" {
dbString = dbFlag
}
return CLIArguments{
ConfigFile: *configFlag,
DbPath: dbString,
}
}
// Read a config from the given CLI arguments as well as the config file specified in the cli args (default is config.toml)
func ReadConfig(cliArgs *CLIArguments) Config {
file, err := os.Open(cliArgs.ConfigFile)
if err != nil {
log.Fatalf("Failed to open file %s with error %v. Does it exist?\n", cliArgs.ConfigFile, err)
}
defer file.Close()
var cfg Config
decoder := toml.NewDecoder(file)
_, err = decoder.Decode(&cfg)
if err != nil {
log.Fatalf("Failed to parse config file %s with error %v\n", cliArgs.ConfigFile, err)
}
godotenv.Load()
err = envconfig.Process("", &cfg)
if err != nil {
log.Fatalf("Failed to overwrite config from env. Error %v\n", err)
}
if cliArgs.DbPath != nil && *cliArgs.DbPath != "" {
cfg.General.DbPath = *cliArgs.DbPath
}
return cfg
}

View file

@ -1,27 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package ap
import (
"net/http"
"github.com/julienschmidt/httprouter"
"gitlab.com/mstarongitlab/linstrom/server"
)
func Register(server *server.Server) {
server.Router.GET("/api/ap/user/:id", GetUser(server))
server.Router.GET("/.well-known/webfinger", WebfingerEndpoint(server))
}
func GetUser(server *server.Server) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {}
}

View file

@ -1,3 +0,0 @@
package mastodon
// https://docs.joinmastodon.org/spec/activitypub/

View file

@ -1,105 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package ap
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/julienschmidt/httprouter"
"gitlab.com/mstarongitlab/goutils/other"
"gitlab.com/mstarongitlab/linstrom/server"
"gitlab.com/mstarongitlab/linstrom/storage"
"gitlab.com/mstarongitlab/linstrom/types"
)
var ErrInvalidResource = errors.New("invalid resource")
type webfingerLink struct {
Rel string `json:"self"`
Type string `json:"type"`
Target url.URL `json:"href"`
}
type WebfingerResponse struct {
Subject string `json:"subject"`
Links []webfingerLink `json:"links"`
}
func WebfingerEndpoint(server *server.Server) func(http.ResponseWriter, *http.Request, httprouter.Params) {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
r.ParseForm()
req := r.FormValue("resource")
if req == "" {
http.NotFound(w, r)
return
}
acc, err := getAccountFromWebfingerResource(req)
if errors.Is(err, ErrInvalidResource) {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if acc.Host.Hostname() != server.Config.General.Domain {
http.Error(w, fmt.Sprintf("wrong host, try %s", &acc.Host), http.StatusNotFound)
return
}
person, err := server.Storage.GetPersonByName(acc.Name)
if err != nil {
if errors.Is(err, storage.ErrNotFound) {
http.Error(w, fmt.Sprintf("Account %s not found", acc), http.StatusNotFound)
return
} else {
http.Error(w, fmt.Sprintf("Internal error: %s, please report", err.Error()), http.StatusInternalServerError)
}
}
response := WebfingerResponse{
Links: []webfingerLink{
{
Rel: "me",
Type: "application/activity+json",
Target: *other.Must(url.Parse(fmt.Sprintf(
"https://%s/api/ap/user/%s",
server.Config.General.Domain,
person.Uid,
))),
},
},
}
json.Marshal(response)
}
}
// turns "acct:bugle@bugle.lol" into { Name: bugle, Host: bugle.lol }
func getAccountFromWebfingerResource(r string) (*types.AccountHandle, error) {
acctCheck, acc, found := strings.Cut(r, ":")
if !found || acctCheck != "acct" {
return nil, ErrInvalidResource
}
name, rawHost, found := strings.Cut(acc, "@")
if !found {
return nil, ErrInvalidResource
}
host, err := url.Parse(rawHost)
if err != nil {
return nil, fmt.Errorf("%w: Invalid url: %w", ErrInvalidResource, err)
}
return &types.AccountHandle{
Name: name,
Host: *host,
}, nil
}

View file

@ -1,20 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package endpoints
import (
"gitlab.com/mstarongitlab/linstrom/endpoints/api/ap"
"gitlab.com/mstarongitlab/linstrom/server"
)
func RegisterAll(s *server.Server) {
ap.Register(s)
}

26
go.mod
View file

@ -1,28 +1,8 @@
module gitlab.com/mstarongitlab/linstrom
go 1.21.5
go 1.22.2
require (
github.com/BurntSushi/toml v1.3.2
github.com/julienschmidt/httprouter v1.3.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/sirupsen/logrus v1.9.3
gorm.io/driver/postgres v1.5.4
gorm.io/gorm v1.25.5
gitlab.com/mstarongitlab/weblogger v0.0.0-20240123135616-d64461e3b20d
)
require (
github.com/MatejLach/astreams v0.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/lib/pq v1.10.9 // indirect
gitlab.com/mstarongitlab/goutils v0.0.0-20240117084827-7f9be06e1b58 // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
github.com/piprate/json-gold v0.5.0 // indirect
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 // indirect
)

57
go.sum
View file

@ -1,53 +1,4 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/MatejLach/astreams v0.6.0 h1:vOrPTVc0ZdByMKSGAB3Sq52C6EFgSikr7phwc/64yxw=
github.com/MatejLach/astreams v0.6.0/go.mod h1:4Ji/fRmm9wAv0JzNWbLULOlpQ8YZ45s9rQjl8Vp85yw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
gitlab.com/mstarongitlab/goutils v0.0.0-20240117084827-7f9be06e1b58 h1:mRislY6modrbqu9ck/3d+P1b/fqxEcuN/s67ULMgATY=
gitlab.com/mstarongitlab/goutils v0.0.0-20240117084827-7f9be06e1b58/go.mod h1:SvqfzFxgashuZPqR9kPwQ9gFA7I1yskZjhmGmY2pAow=
gitlab.com/mstarongitlab/weblogger v0.0.0-20240123135616-d64461e3b20d h1:ixTYuViFIDQh9fs3xyc8sPKBr7dU7T98rs4kqM0q51k=
gitlab.com/mstarongitlab/weblogger v0.0.0-20240123135616-d64461e3b20d/go.mod h1:8G+BrXVs97wI7W+Z4E8M2MXFFKKy01cVnFazoCpNg9Y=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
gorm.io/gorm v1.25.5 h1:zR9lOiiYf09VNh5Q1gphfyia1JpiClIWG9hQaxB/mls=
gorm.io/gorm v1.25.5/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
github.com/piprate/json-gold v0.5.0 h1:RmGh1PYboCFcchVFuh2pbSWAZy4XJaqTMU4KQYsApbM=
github.com/piprate/json-gold v0.5.0/go.mod h1:WZ501QQMbZZ+3pXFPhQKzNwS1+jls0oqov3uQ2WasLs=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35 h1:J9b7z+QKAmPf4YLrFg6oQUotqHQeUNWwkvo7jZp1GLU=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=

View file

@ -1,27 +0,0 @@
package guardian
import (
"net/http"
"gitlab.com/mstarongitlab/linstrom/config"
"gitlab.com/mstarongitlab/linstrom/storage"
)
type GuardianMiddleware struct {
nextHandler http.Handler
config *config.Config
storage *storage.Storage
}
func NewMiddleware(nextHandler http.Handler, cfg *config.Config, s *storage.Storage) *GuardianMiddleware {
return &GuardianMiddleware{
nextHandler: nextHandler,
config: cfg,
storage: s,
}
}
func (gm *GuardianMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// TODO: Add guardian filtering stuff here, see Planka
gm.nextHandler.ServeHTTP(w, r)
}

30
main.go
View file

@ -1,30 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package main
import (
"fmt"
"gitlab.com/mstarongitlab/linstrom/config"
"gitlab.com/mstarongitlab/linstrom/storage"
)
func main() {
cliArgs := config.ReadCLIArguments()
fmt.Println(cliArgs)
cfg := config.ReadConfig(&cliArgs)
fmt.Println(cfg)
storage, err := storage.NewStorage(&cfg)
if err != nil {
panic(err)
}
fmt.Println(storage.GetFollowersFor("Placeholder"))
}

View file

@ -1,37 +0,0 @@
package server
import (
"net/http"
"github.com/julienschmidt/httprouter"
"gitlab.com/mstarongitlab/linstrom/config"
"gitlab.com/mstarongitlab/linstrom/guardian"
"gitlab.com/mstarongitlab/linstrom/storage"
"gitlab.com/mstarongitlab/weblogger"
)
type Server struct {
// Register routes on here
Router *httprouter.Router
// Where to read and write stuff from
Storage *storage.Storage
// Global config. READ ONLY
Config *config.Config
// Final handler given to http.ListenAndServe
// Can be any router or middleware wrapped around a router
handler http.Handler
}
func NewServer(cfg *config.Config, s *storage.Storage) *Server {
router := httprouter.New()
return &Server{
Router: router,
Storage: s,
Config: cfg,
handler: guardian.NewMiddleware(weblogger.LoggingMiddleware(router), cfg, s),
}
}
func (s *Server) Run() {
http.ListenAndServe(":8080", s.handler)
}

View file

@ -1,31 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package storage
type Follow struct {
Follower Person
FollowerID uint
Follows Person
FollowsID uint
}
func (s *Storage) GetFollowersFor(url string) ([]Follow, error) {
var pHit int64
s.Db.Where("url = ?", url).Count(&pHit)
if pHit <= 0 {
return nil, ErrNotFound
}
var followed *Person = &Person{}
s.Db.Table("people").First(&followed, "url = ?", url)
var f []Follow = make([]Follow, 0)
s.Db.Table("follows").Find(&f, "follows_id = ?", followed.ID)
return f, nil
}

View file

@ -1,37 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package storage
import (
"github.com/lib/pq"
"gorm.io/gorm"
)
type Note struct {
gorm.Model // Includes primary key (uid) as well as various time info
// Public data
Uid string
Content string
CreatorID uint
Creator Person
Instance string // url
Local bool
Tags pq.StringArray `gorm:"type:text[]"`
RepliesTo *Note
RepliesToID *uint
DeliverTo pq.StringArray `gorm:"type:text[]"` // url
// Private data
// None yet
}

View file

@ -1,71 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package storage
import (
"errors"
"github.com/MatejLach/astreams"
"gorm.io/gorm"
)
type Person struct {
gorm.Model // Includes primary key (uid) as well as various time info
// Public data
Uid string
ApID string
Name string
Instance string // url
Local bool
PublicKey string
Url string
ManuallyApprovesFollowers bool
// Private data, set if local == true
Mail *string // mail
PwHash *string
Inbox string // url
Outbox string // url
PrivateKey *string // private key
}
func (pdb *Person) IntoAPPerson() (*astreams.Person, error) {
var actor astreams.Actor
actor.PublicKey = &astreams.PublicKey{
ID: pdb.ApID,
Owner: pdb.Url,
PublicKeyPem: pdb.PublicKey,
}
actor.Inbox = &astreams.StringWithOrderedCollection{
URL: pdb.Inbox,
}
actor.Outbox = &astreams.StringWithOrderedCollection{
URL: pdb.Outbox,
}
actor.ManuallyApprovesFollowers = pdb.ManuallyApprovesFollowers
actor.PreferredUsername = pdb.Name
return nil, errors.New("unimplemented")
}
// Gets a person from the db by name
func (s *Storage) GetPersonByName(name string) (*Person, error) {
var nrFound int64
s.Db.Table("people").Where("name = ?", name).Count(&nrFound)
if nrFound <= 0 {
return nil, ErrNotFound
}
var p Person
s.Db.Table("people").Where("name = ?", name).First(&p)
return &p, nil
}

View file

@ -1,55 +0,0 @@
// Copyright (c) 2024 mStar
//
// Licensed under the EUPL, Version 1.2
//
// You may not use this work except in compliance with the Licence.
// You should have received a copy of the Licence along with this work. If not, see:
// <https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12>.
// See the Licence for the specific language governing permissions and limitations under the Licence.
//
package storage
import (
"errors"
"fmt"
"gitlab.com/mstarongitlab/linstrom/config"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var ErrNotFound = errors.New("entry not found")
type Storage struct {
Db *gorm.DB
}
func NewDb(cfg *config.Config) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(cfg.General.DbPath))
if err != nil {
return nil, fmt.Errorf("failed to connect to db %s: %w", cfg.General.DbPath, err)
}
err = db.AutoMigrate(
&Person{},
&Note{
Tags: make([]string, 0),
DeliverTo: make([]string, 0),
},
&Follow{},
)
if err != nil {
return nil, fmt.Errorf("failed to apply migrations: %w", err)
}
return db, nil
}
func NewStorage(cfg *config.Config) (*Storage, error) {
db, err := NewDb(cfg)
if err != nil {
return nil, fmt.Errorf("failed to create database connection while creating storage: %w", err)
}
return &Storage{
Db: db,
}, nil
}

View file

@ -1,15 +0,0 @@
package types
import (
"fmt"
"net/url"
)
type AccountHandle struct {
Name string
Host url.URL
}
func (a *AccountHandle) String() string {
return fmt.Sprintf("%s@%s", a.Name, a.Host.String())
}