add some works

This commit is contained in:
inhosin 2019-09-07 17:54:53 +03:00
parent cd9ec0c774
commit 8807e5aaab
22 changed files with 1074 additions and 3 deletions

1
.gitignore vendored
View File

@ -29,3 +29,4 @@ tests/db
# vendor dir
vendor/
trash/

114
Gopkg.lock generated
View File

@ -1,9 +1,121 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
digest = "1:3ee1d175a75b911a659fbd860060874c4f503e793c5870d13e5a0ede529a63cf"
name = "github.com/gin-contrib/sse"
packages = ["."]
pruneopts = "UT"
revision = "54d8467d122d380a14768b6b4e5cd7ca4755938f"
version = "v0.1.0"
[[projects]]
digest = "1:d8bd2a337f6ff2188e08f72c614f2f3f0fd48e6a7b37a071b197e427d77d3a47"
name = "github.com/gin-gonic/gin"
packages = [
".",
"binding",
"internal/json",
"render",
]
pruneopts = "UT"
revision = "b75d67cd51eb53c3c3a2fc406524c940021ffbda"
version = "v1.4.0"
[[projects]]
digest = "1:573ca21d3669500ff845bdebee890eb7fc7f0f50c59f2132f2a0c6b03d85086a"
name = "github.com/golang/protobuf"
packages = ["proto"]
pruneopts = "UT"
revision = "6c65a5562fc06764971b7c5d05c76c75e84bdbf7"
version = "v1.3.2"
[[projects]]
digest = "1:709cd2a2c29cc9b89732f6c24846bbb9d6270f28ef5ef2128cc73bd0d6d7bff9"
name = "github.com/json-iterator/go"
packages = ["."]
pruneopts = "UT"
revision = "27518f6661eba504be5a7a9a9f6d9460d892ade3"
version = "v1.1.7"
[[projects]]
digest = "1:31e761d97c76151dde79e9d28964a812c46efc5baee4085b86f68f0c654450de"
name = "github.com/konsorten/go-windows-terminal-sequences"
packages = ["."]
pruneopts = "UT"
revision = "f55edac94c9bbba5d6182a4be46d86a2c9b5b50e"
version = "v1.0.2"
[[projects]]
digest = "1:36325ebb862e0382f2f14feef409ba9351271b89ada286ae56836c603d43b59c"
name = "github.com/mattn/go-isatty"
packages = ["."]
pruneopts = "UT"
revision = "e1f7b56ace729e4a73a29a6b4fac6cd5fcda7ab3"
version = "v0.0.9"
[[projects]]
digest = "1:33422d238f147d247752996a26574ac48dcf472976eda7f5134015f06bf16563"
name = "github.com/modern-go/concurrent"
packages = ["."]
pruneopts = "UT"
revision = "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94"
version = "1.0.3"
[[projects]]
digest = "1:e32bdbdb7c377a07a9a46378290059822efdce5c8d96fe71940d87cb4f918855"
name = "github.com/modern-go/reflect2"
packages = ["."]
pruneopts = "UT"
revision = "4b7aa43c6742a2c18fdef89dd197aaae7dac7ccd"
version = "1.0.1"
[[projects]]
digest = "1:04457f9f6f3ffc5fea48e71d62f2ca256637dee0a04d710288e27e05c8b41976"
name = "github.com/sirupsen/logrus"
packages = ["."]
pruneopts = "UT"
revision = "839c75faf7f98a33d445d181f3018b5c3409a45e"
version = "v1.4.2"
[[projects]]
digest = "1:1d4ed6179f215ac661d5c447022b4d10074fbd8829b55e2d405d2a65496a278b"
name = "github.com/ugorji/go"
packages = ["codec"]
pruneopts = "UT"
revision = "23ab95ef5dc3b70286760af84ce2327a2b64ed62"
version = "v1.1.7"
[[projects]]
branch = "master"
digest = "1:767025ef5997eeba9a3b7fba288b8b9e71b44f0e3a2ec6dcd01f144d3dc8f173"
name = "golang.org/x/sys"
packages = ["unix"]
pruneopts = "UT"
revision = "749cb33beabd9aa6d3178e3de05bcc914f70b2bf"
[[projects]]
digest = "1:cbc72c4c4886a918d6ab4b95e347ffe259846260f99ebdd8a198c2331cf2b2e9"
name = "gopkg.in/go-playground/validator.v8"
packages = ["."]
pruneopts = "UT"
revision = "5f1438d3fca68893a817e4a66806cea46a9e4ebf"
version = "v8.18.2"
[[projects]]
digest = "1:4d2e5a73dc1500038e504a8d78b986630e3626dc027bc030ba5c75da257cdb96"
name = "gopkg.in/yaml.v2"
packages = ["."]
pruneopts = "UT"
revision = "51d6538a90f86fe93ac480b35f37b2be17fef232"
version = "v2.2.2"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
input-imports = []
input-imports = [
"github.com/gin-gonic/gin",
"github.com/sirupsen/logrus",
]
solver-name = "gps-cdcl"
solver-version = 1

152
README.md
View File

@ -1,3 +1,151 @@
# subhub
# Subhub
An _experimental_ (Read: not-usable or in anyway done) distributed/federated podcasting platform based on [ActivityPub.](https://raw.githubusercontent.com/w3c/activitypub/gh-pages/activitypub-tutorial.txt)
## Usage
Getting started:
Ensure that you're using go11 with go-modules turned on.
```sh
export GO111MODULE=on # Put this in your .zshrc or .bash_profile or whatnot
```
Clone/Download the project with:
```sh
go get git.macaw.me/inhosin/subhub
```
Building a binary with make (or [mmake](https://github.com/tj/mmake) if you're fancy):
```sh
make
```
Building and then running that binary:
```sh
make run
```
Running tests:
```sh
make test
```
Setting up your database (this works best if you have postgres already [running locally](https://postgresapp.com/)):
```sh
make database
```
Creating a new migration in `db/migrations`:
```sh
make migration NAME=some_name_here
```
## Learning about ActivityPub
![explaination](https://i.imgur.com/ShgecWe.png)
### Basic Description
ActivityPub gives every user (or `actor` in it's vocab) on a server an "inbox" and an "outbox". But these are really just endpoints:
```
https://myactpub.site/activity/user/flaque/inbox
https://myactpub.site/activity/user/flaque/outbox
```
ActivityPub asks that you accept `GET` and `POST` requests to these endpoints where a `POST` tells a the server to put that in a user's queue or feed and `GET` lets the user retrieve info from the feed.
You send messages called `ActivityStreams` that are really just a special spec of JSON:
```json
{"@context": "https://www.w3.org/ns/activitystreams",
"type": "Create",
"id": "https://social.example/alyssa/posts/a29a6843-9feb-4c74-a7f7-081b9c9201d3",
"to": ["https://chatty.example/ben/"],
"author": "https://social.example/alyssa/",
"object": {"type": "Note",
"id": "https://social.example/alyssa/posts/49e2d03d-b53a-4c4c-a95c-94a6abf45a19",
"attributedTo": "https://social.example/alyssa/",
"to": ["https://chatty.example/ben/"],
"content": "Say, did you finish reading that book I lent you?"}
```
#### Objects, Actors, and Activities
(**Note:** Pubcast uses a slightly different internal naming than ActivityPub. To have more understandable code in the context of podcasts, ActivityPub's `Organization` actor type is a `Show` inside Pubcast. Additionally, the `Object` type is a Pubcast `Episode`.)
ActivityPub is based on [a formalized vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/) of data types, actions and folks doing the actions.
An `Object` is a generic data type written in JSON:
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Object",
"id": "http://www.test.example/object/1",
"name": "A Simple, non-specific object"
}
```
Objects have [a set collection of formalized properties](https://www.w3.org/TR/activitystreams-vocabulary/#properties) such as `id`, `name`, `url`, etc but you technically can create your own. Objects serve as a base type for other Activity Steam's core set of types.
For example, there are a set of [actor types](https://www.w3.org/TR/activitystreams-vocabulary/#actor-types) that themselves are `Objects`.
```json
/* A "Person" actor type */
{
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Person",
"name": "Sally Smith"
}
```
[Activities](https://www.w3.org/TR/activitystreams-vocabulary/#h-activity-types) are also subtypes of `Object`, and are used to describe relationships between objects. Some examples of activities include:
- Accept
- Create
- Move
- Question
- Undo
- Follow
- View
An `Activity` json might look something like this:
```json
{
"@context": "https://www.w3.org/ns/activitystreams",
"summary": "Sally created a note",
"type": "Create",
"actor": {
"type": "Person",
"name": "Sally"
},
"object": {
"type": "Note",
"name": "A Simple Note",
"content": "This is a simple note"
}
}
```
### Links
- [ActivityPub tutorial](https://raw.githubusercontent.com/w3c/activitypub/gh-pages/activitypub-tutorial.txt)
- [ActivityPub.rocks explaination](https://activitypub.rocks/)
- [W3 ActivityPub Spec](https://www.w3.org/TR/activitypub/)
- [W3 ActivityPub Vocabulary Spec](https://www.w3.org/TR/activitystreams-vocabulary/)
- [Other Golang implementations of this spec](https://github.com/go-fed/activity#who-is-using-this-library-currently)
## Design
### Hub Object
Subscriber hubs

91
TZ.md Normal file
View File

@ -0,0 +1,91 @@
## Описание компонентов движка
### Бэкенд
Работает в виде демона, реализующего 3 вида API:
1) Activitypub для межсерверного взаимодействия
2) Публичный API для доступа к тредам (будет использоваться фронтендом для показа тредов обсуждения на публичной странице сообщества)
3) API администрирования (будет использоваться административной частью фронтенда, в которую будут логиниться пользователи-модераторы групп и администратор сервера)
Основные сущности, которые таким образом реализует бэкенд:
Акторы - согласно понятию актора из Устава, механизм публикации и подписки в рамках Fediverse. Имеют адрес вида who@where.tld. В нашем случае, акторы имеют тип "Group", и в терминолгогии проекта называются "хабами"
Хаб - специальный актор, осуществляющий групповое общение пользователей Fediverse.
Аккаунт модератора - рядовой аккаунт в системе SubHub, который дает право создавать группы и управлять ими.
Аккаунт администратора - административный аккаунт в системе SubHub, который дает право производить административные действия над сервером и аккаунтами модераторов.
Общий алгоритм работы сервера:
1) Сервер получает оповещение об упоминании актора одного из акторов на нем;
2) Если упоминаемый актор действительно существует, идет прверка свойств хаба (см. раздел о возможных видах хабов). Здесь рассмотрим случай, где члены хаба = подписчики;
3) Проверяем сообщение, в котором упоминается актор. Если сообщение первое в треде (у него не имеется, in-reply-to), записываем его в БД как новый тред, и объявляем активность Announce относительно этого сообщения. Таким образом, все подписчики хаба получат оригинальное сообщение, на которое можно будет отвечать и реагировать;
4) Если сообщение с упоминанием не первое в треде, проверяем (рекурсивно), не принадлежит ли оно к какому-нибудь треду, уже присутствующему на сервере. Если принадлежит, присоединяем его и все предыдущие сообщения к треду в базе данных;
5) Если сообщение с упоминанием не первое в треде, и не принадлежит к ни одному существующему на сервере треду, то проверяем, разрешено ли "прикалывание" (?) тредов пользователям. Если да, то забираем это сообщение, и рекурсивно все предыдущие вплоть до первого. Первое сообщение обозначаем в базе как начало треда, с информацией о том,от кого было "прикалывающее сообщение";
6) За существующими тредами продолжаем наблюдать на предмет появления новых сообщений, и записываем в базу если таковые находятся;
7) Клиентскому фронтенду по запросу на API отдаем информацию о группе, список ее тредов, и, собственно, сами треды для просмотра в браузере.
8) Административному фронтенду позволяем авторизовываться как модератор или администратор, и предоставляем в зависимости от этого управление сервером целиком или управление только своими группами.
9) Производим регулярную проверку хотя бы первых сообщений в тредах, на предмет новых реакций - обновляем счетчики реакций соответственно.
Таким образом, общий алгоритм использования следующий:
- Администратор устанавливает сервер. Допустим, subhub.com
- Модератор регистрируется на сервере и создает группу (или несколько). Выбирает группе аватар, фон, возможно CSS-тему, и т.д. Допустим, группа rf@subhub.com
- Пользователи присоединяются к группе, путем подписки на нее.
- Пользователь alisa@mastodon.social, подписчица rf@subhub.com, создает сообщение, первое в треде, с упомининием rf@subhub.com
- Сервер subhub.com замечает это, записывает сообщение-топикстартер в базу, и дает Announce всем подписчикам rf@subhub.com
- Все последующие сообщения в треде сервер будет записывать в базу, где-то получая их по упоминаниям, а где-то где упоминание потеряли, захватывать их самостоятельно (если это позволено модератором и администратором)
"Прикалывание" тредов:
- Пользователь pazan@mastodon.ml наткнулся на интересное обсуждение в Fediverse, и решил обнародовать этот тред для членов группы rf@subhub.com, в которой разрешено прикалывание;
- Для этого он создает сообщение-ответ в треде, в которм упоминает rf@subhub.com. Shubub.com получает активность упоминания;
- Subhub.com ищет по цепочке in-reply-to первое сообщение в треде, записывает его в базу и издает Announce в отношении него, с пометкой о том, что данный тред был приколот pazan@mastodon.ml, а также берет весь тред в базу целиком, со всеми существующими ветвями.
- Все последующие сообщения в треде сервер будет записывать в базу, где-то получая их по упоминаниям, а где-то где упоминание потеряли, захватывать их самостоятельно (если это позволено модератором и администратором)
Виды хабов:
- Доска объявлений: Тред может создать кто угодно, членство не обязательно;
- Группа: Тред может создать только подписчик;
- Клуб: Тред может создать только подписчик, при этом подписчиков необходимо подтверждать модератору;
- Канал: Тред может создать лишь строго ограниченный список лиц, заданный модератором.
Прикалывание сообщений и принудительный захват тредов - опции, определяемые модератором. Администратор может запрещать эти опции.
### Фронтенд
Фронтенд делится на две части: публичный и административный
#### Публичный фронтенд
Шлет запросы на публичный API с целью получения данных о хабах и тредах. Таким образом, осуществляет просмотр архива обсуждений на хабах. Имеет интерфейс, похожий на Reddit, позволяет осуществлять сортировку по дате добавления, дате последнего сообщения, по популярности, и т.д. Имеет два основных вида:
- Страница хаба. На ней находится название хаба, аватар и фон хаба, адрес хаба, количество подписчиков, адрес модератора для связи (задается на странице модератора, принимает на вход адрес актора - в другом соцдвижке, не-Subhub), описание хаба (задается модератором хаба), его тип. Помимо всей информации о хабе, имеет список тредов, который можно сортировать, и осуществлять поиск.
- Страница треда. На ней находится главным образом та же самая информация, что на странице хаба, однако вместо списка тредов на странице находится обсуждение, в виде reddit-овского дерева. Каждое сообщение в треде (помимо обычной информации вроде тела сообщения и количества реакций) имеет ссылку на оригинал, на сервере человека, его написавшего.
Для его просмотра не требуется авторизация.
#### Административный фронтенд
Авторизует пользователя, и шлет запросы на административный API, который позволяет:
Модераторам -
- Определить личный параметры (вроде адреса своего актора для связи)
- Создавать хабы
- Редактировать параметры хаба (аватар, фон, описание, тип, функции прикалывания и захвата тредов)
- Редактировать список подписчиков (иногда нужно отписать или заблокировать какого-нибудь хулигана, или даже заблокировать домен)
- Редактировать список тредов хаба и сами треды (иногда нужно удалить тред или сообщение)
- В случае, если хаб - канал, определять список доступа к созданию тредов
- Закрывать/удалять хабы
Администраторам -
- Все, что можно модераторам
- Определять параметры сервера (его описание, правила, оформление, технические опции, например, возможность использовать захват тредов и прикалывание)
- Просматирвать и редактировать список модераторов на сервере
- Блокировка доменов и пользователей по всему серверу
В итоге получаем работающий форум на отдельном движке в Fediverse:
- Модераторы создают хабы;
- Пользователи других серверовы пишут в хабы
- За обсуждением можно наблюдать, даже не находясь в Fediverse.
Вот значимая часть, с описанием архитектуры проги. Жуткий сумбур, и это явно не ТЗ, но пока то, что есть.

3
activitypub/README.md Normal file
View File

@ -0,0 +1,3 @@
# ActivityPub
This package contains functions for handling the ActivityPub spec.

145
activitypub/activity.go Normal file
View File

@ -0,0 +1,145 @@
package activitypub
import "time"
const (
Namespace = "https://www.w3.org/ns/activitystreams"
toPublic = "https://www.w3.org/ns/activitystreams#Public"
)
var Extensions = map[string]string{
"sc": "http://schema.org#",
"commentsEnabled": "sc:Boolean",
}
// Activity describes actions that have either already occurred, are in the
// process of occurring, or may occur in the future.
type Activity struct {
BaseObject
Actor string `json:"actor"`
Published time.Time `json:"published,omitempty"`
To []string `json:"to,omitempty"`
CC []string `json:"cc,omitempty"`
Object *Object `json:"object"`
}
type FollowActivity struct {
BaseObject
Actor string `json:"actor"`
Published time.Time `json:"published,omitempty"`
To []string `json:"to,omitempty"`
CC []string `json:"cc,omitempty"`
Object string `json:"object"`
}
// NewCreateActivity builds a basic Create activity that includes the given
// Object and the Object's AttributedTo property as the Actor.
func NewCreateActivity(o *Object) *Activity {
a := Activity{
BaseObject: BaseObject{
Context: []interface{}{
Namespace,
Extensions,
},
ID: o.ID,
Type: "Create",
},
Actor: o.AttributedTo,
Object: o,
Published: o.Published,
}
return &a
}
// NewUpdateActivity builds a basic Update activity that includes the given
// Object and the Object's AttributedTo property as the Actor.
func NewUpdateActivity(o *Object) *Activity {
a := Activity{
BaseObject: BaseObject{
Context: []interface{}{
Namespace,
Extensions,
},
ID: o.ID,
Type: "Update",
},
Actor: o.AttributedTo,
Object: o,
Published: o.Published,
}
return &a
}
// NewDeleteActivity builds a basic Delete activity that includes the given
// Object and the Object's AttributedTo property as the Actor.
func NewDeleteActivity(o *Object) *Activity {
a := Activity{
BaseObject: BaseObject{
Context: []interface{}{
Namespace,
},
ID: o.ID,
Type: "Delete",
},
Actor: o.AttributedTo,
Object: o,
}
return &a
}
// NewFollowActivity builds a basic Follow activity.
func NewFollowActivity(actorIRI, followeeIRI string) *FollowActivity {
a := FollowActivity{
BaseObject: BaseObject{
Context: []interface{}{
Namespace,
},
Type: "Follow",
},
Actor: actorIRI,
Object: followeeIRI,
}
return &a
}
// NewNoteObject creates a basic Note object that includes the public
// namespace in IRIs it's addressed to.
func NewNoteObject() *Object {
o := Object{
BaseObject: BaseObject{
Type: "Note",
},
To: []string{
toPublic,
},
}
return &o
}
// NewArticleObject creates a basic Article object that includes the public
// namespace in IRIs it's addressed to.
func NewArticleObject() *Object {
o := Object{
BaseObject: BaseObject{
Type: "Article",
},
To: []string{
toPublic,
},
}
return &o
}
// NewAnounceObject creates a basic Anounce object that includes the public
// namespace in IRIs it's addressed to.
func NewAnounceObject() *Object {
o := Object{
BaseObject: BaseObject{
Type: "Anounce",
},
To: []string{
toPublic,
},
}
return &o
}

57
activitypub/person.go Normal file
View File

@ -0,0 +1,57 @@
package activitypub
// An Person actor
// Ref: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-person
type Person struct {
BaseObject
Inbox string `json:"inbox"`
Outbox string `json:"outbox"`
PreferredUsername string `json:"preferredUsername"`
URL string `json:"url"`
Name string `json:"name"`
Icon Image `json:"icon"`
Following string `json:"following"`
Followers string `json:"followers"`
Summary string `json:"summary"`
PublicKey PublicKey `json:"publicKey"`
Endpoints Endpoints `json:"endpoints"`
}
// NewPerson creates an ActivityPub Person
func NewPerson(id string) *Person {
p := Person{
BaseObject: BaseObject{
Type: "Person",
Context: []interface{}{
Namespace,
},
ID: id,
},
Following: id + "/following",
Followers: id + "/followers",
Inbox: id + "/inbox",
Outbox: id + "/outbox",
}
return &p
}
// AddPubKey add public key to Person
func (p *Person) AddPubKey(k []byte) {
p.Context = append(p.Context, "https://w3id.org/security/v1")
p.PublicKey = PublicKey{
ID: p.ID + "#main-key",
Owner: p.ID,
PublicKeyPEM: string(k),
}
}
// SetPrivateKey set private key to Person
func (p *Person) SetPrivateKey(k []byte) {
p.PublicKey.privateKey = k
}
// GetPrivateKey get private key
func (p *Person) GetPrivateKey() []byte {
return p.PublicKey.privateKey
}

107
activitypub/vocab.go Normal file
View File

@ -0,0 +1,107 @@
package activitypub
import (
"fmt"
"net/url"
"time"
)
const context = "https://ww.w3.org/ns/activitystreams"
type (
BaseObject struct {
Context []interface{} `json:"@context,omitempty"`
Type string `json:"type"`
ID string `json:"id"`
}
PublicKey struct {
ID string `json:"id"`
Owner string `json:"owner"`
PublicKeyPEM string `json:"publicKeyPem"`
privateKey []byte
}
Endpoints struct {
SharedInbox string `json:"sharedInbox,omitempty"`
}
Image struct {
Type string `json:"type"`
MediaType string `json:"mediaType"`
URL string `json:"url"`
}
// Object is the primary base type for the Activity Streams vocabulary.
Object struct {
BaseObject
Published time.Time `json:"published"`
Summary *string `json:"summary,omitempty"`
InReplyTo *string `json:"inReplyTo"`
URL string `json:"url"`
AttributedTo string `json:"attributedTo"`
To []string `json:"to"`
CC []string `json:"cc,omitempty"`
Name string `json:"name,omitempty"`
Content string `json:"content"`
ContentMap map[string]string `json:"contentMap,omitempty"`
//Tag []Tag `json:"tag"`
// Extensions
CommentsEnabled bool `json:"commentsEnabled"`
}
Group struct {
Context string `json:"@context"`
ID *url.URL `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
)
type OrderedCollection struct {
BaseObject
TotalItems int `json:"totalItems"`
First string `json:"first"`
Last string `json:"last,omitempty"`
}
func NewOrderedCollection(accountRoot, collType string, items int) *OrderedCollection {
oc := OrderedCollection{
BaseObject: BaseObject{
Context: []interface{}{
Namespace,
},
ID: accountRoot + "/" + collType,
Type: "OrderedCollection",
},
First: accountRoot + "/" + collType + "?page=1",
TotalItems: items,
}
return &oc
}
type OrderedCollectionPage struct {
BaseObject
TotalItems int `json:"totalItems"`
PartOf string `json:"partOf"`
Next string `json:"next,omitempty"`
Prev string `json:"prev,omitempty"`
OrderedItems []interface{} `json:"orderedItems,omitempty"`
}
func NewOrderedCollectionPage(accountRoot, collType string, items, page int) *OrderedCollectionPage {
ocp := OrderedCollectionPage{
BaseObject: BaseObject{
Context: []interface{}{
Namespace,
},
ID: fmt.Sprintf("%s/%s?page=%d", accountRoot, collType, page),
Type: "OrderedCollectionPage",
},
TotalItems: items,
PartOf: accountRoot + "/" + collType,
Next: fmt.Sprintf("%s/%s?page=%d", accountRoot, collType, page+1),
}
return &ocp
}

7
app/account.go Normal file
View File

@ -0,0 +1,7 @@
package app
type SSHKey struct {
ID string `json:"id"`
Private []byte `json:"prv,omitempty"`
Public []byte `json:"pub,omitempty"`
}

46
app/app.go Normal file
View File

@ -0,0 +1,46 @@
package app
import (
"database/sql"
"flag"
"log"
"os"
)
const (
serverName = "Sub Hub"
softwareVer = "0.0.1"
configFile = "config.json"
)
type app struct {
db *sql.DB
cfg *config
}
type config struct {
Host string `json:"host"`
Port int `json:"port"`
MySQLConnStr string `json:"mysql_conn_str"`
Name string `json:"instance_name"`
}
func Serve() {
app := &app{
cfg: &config{},
}
var newUser, newPass string
flag.IntVar(&app.cfg.Port, "p", 8090, "Port to start server on")
flag.StringVar(&app.cfg.Host, "h", "", "Serer base URL")
// options for creating a new user
flag.StringVar(&newUser, "user", "", "New user's username. Should be paired with --pass")
flag.StringVar(&newPass, "pass", "", "Password for new user. Should be paired with --user")
flag.Parse()
if app.cfg.Host == "" || os.Getenv("SH_MYSQL_CONNECTION") == "" {
log.Printf("Reding %s", configFile)
//f, err := ioutil.ReadFile(configFile)
}
}

6
config/constants.go Normal file
View File

@ -0,0 +1,6 @@
package config
const (
ServerHostName = "subhub.hostname"
ServerPort = "subhub.port"
)

32
data/data.go Normal file
View File

@ -0,0 +1,32 @@
package data
import (
"database/sql"
"fmt"
)
var (
pool *sql.DB
)
func GetDB() *sql.DB {
if pool == nil {
// TODO write text
panic("")
}
return pool
}
func NewDB() (*sql.DB, error) {
const (
host = "localhost"
user = "admin"
pass = "Bambukalo2201"
dbname = "subhub"
)
dbConnString := fmt.Sprintf("%s:%s@%s/%s?charset=utf8mb4&parseTime=true",
user, pass, host, dbname)
return sql.Open("mysql", dbConnString)
}

52
data/models/group.go Normal file
View File

@ -0,0 +1,52 @@
package models
import (
"database/sql"
"time"
slugify "github.com/gosimple/slug"
)
// Group is a collection of Hubs (equivalent to ActivityPub Organizations)
// Refers to the https://www.w3.org/TR/activitystreams-vocabulary/#dfn-group
// Also refers to the Groups table in the database
type Group struct {
Slug string `json:"slug"`
Name string `json:"name"`
Note string `json:"note"`
Type string
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// GetGroup returns a single Group object or nil
func GetGroup(db *sql.DB, slug string) (*Group, error) {
row := db.QueryRow(`
select slug, name, note, created_at, updated_at
from groups where slug = $1
`, slug)
var group Group
err := row.Scan(&group.Slug, &group.Name, &group.Note, &group.CreatedAt, &group.UpdatedAt)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
return &group, nil
}
// PutGroup put new group into db and return slur or error
func PutGroup(db *sql.DB, name string, note string) (string, error) {
// TODO: make slugify "github.com/gosimple/slug"
slug := slugify.MakeLang(name, "en")
query := `
insert into groups(slug, name, note)
values ($1, $2, $3)
`
_, err := db.Exec(query, slug, name, note)
return slug, err
}

6
handlers/README.md Normal file
View File

@ -0,0 +1,6 @@
# Handlers
This folder contains [Mux](https://github.com/gorilla/mux) route handlers. If you've worked in webservers in other languages, it's similar to the concept of controllers.
It's common to see a handler take some information and spit it to something in `lib`. This is because handlers convert web-server knowledge to buisness-logic rather than do everything at once.

41
handlers/groups/get.go Normal file
View File

@ -0,0 +1,41 @@
package groups
import (
"fmt"
"git.macaw.me/inhosin/subhub/activitypub"
"git.macaw.me/inhosin/subhub/data"
"git.macaw.me/inhosin/subhub/data/models"
"git.macaw.me/inhosin/subhub/handlers"
"github.com/gin-gonic/gin"
"net/http"
)
// Get returns a Group
//
// Expects a `{name}` url variable
// in the route: `/api/group/:name`
func Get(c *gin.Context) {
name := c.Param("name")
if name == "" {
c.String(http.StatusBadRequest, "Bad request! No name in URL")
return
}
group, err := models.GetGroup(data.GetDB(), name)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
if group == nil {
c.String(http.StatusNotFound, fmt.Sprintf("%s does not exits on server", name))
return
}
url := handlers.GetFullHostName() + "/activity/group/" + group.Slug
actor := activitypub.NewPerson(url)
actor.PreferredUsername = group.Name
actor.Name = group.Name
actor.Summary = group.Note
c.JSON(http.StatusOK, actor)
}

32
handlers/groups/post.go Normal file
View File

@ -0,0 +1,32 @@
package groups
import (
"git.macaw.me/inhosin/subhub/data"
"git.macaw.me/inhosin/subhub/data/models"
"github.com/gin-gonic/gin"
"net/http"
)
type createGroupRequest struct {
Name string `json:"name"`
Note string `json:"note"`
}
type createGroupResponse struct {
Slug string `json:"slug"`
}
// Create adds an group to the database
func Create(c *gin.Context) {
var body createGroupRequest
c.BindJSON(&body)
db := data.GetDB()
slug, err := models.PutGroup(db, body.Name, body.Note)
if err != nil {
c.String(http.StatusInternalServerError, err.Error())
return
}
resp := createGroupResponse{Slug: slug}
c.JSON(http.StatusOK, resp)
}

7
handlers/path.go Normal file
View File

@ -0,0 +1,7 @@
package handlers
// GetFullHostName return string
func GetFullHostName() string {
// TODO make it at config
return "http://" + "localhost" + ":8090"
}

View File

@ -0,0 +1,23 @@
# What's Webfinger?
[Webfinger](https://en.wikipedia.org/wiki/WebFinger), a protocol for discovering objects on the server. It's used by Mastodon and is important for interop'ing with Mastodon (and most ActivityPub servers).
It lives at a special route: `GET /.well-known/webfinger`.
We can expect a webfinger response to always looks something like this:
```json
{
"subject": "acct:alice@my-example.com",
"links": [
{
"rel": "self",
"type": "application/activity+json",
"href": "https://my-example.com/actor"
}
]
}
```
In this case, `alice` is the ActivityPub Organization slug, and `my-example.com` is the domain of the server.

View File

@ -0,0 +1,40 @@
package webfinder
import (
"git.macaw.me/inhosin/subhub/handlers"
"net/mail"
"strings"
)
func newBadAddressError(address string) *badAddressError {
return &badAddressError{address: address}
}
func slugOf(address string) string {
fragment := strings.Split(address, "@")
return fragment[0]
}
func atAddress(address string) (*Resource, error) {
// foo@bar.org => foo
parser := mail.AddressParser{}
_, err := parser.Parse(address)
if err != nil {
return nil, newBadAddressError(address)
}
// foo@bar.org => foo
slug := slugOf(address)
domain := handlers.GetFullHostName()
return &Resource{
Subject: address,
Links: []Link{
{
Rel: "self",
Type: "application/activity+json",
HRef: domain + "/api/group/" + slug,
},
},
}, nil
}

View File

@ -0,0 +1,26 @@
package webfinder
type Resource struct {
Subject string `json:"subject,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Properties map[string]string `json:"properties,omitempty"`
Links []Link `json:"links"`
}
type Link struct {
HRef string `json:"href"`
Type string `json:"type,omitempty"`
Rel string `json:"rel"`
Properties map[string]*string `json:"properties,omitempty"`
Titles map[string]string `json:"titles,omitempty"`
}
type Rel string
type badAddressError struct {
address string
}
func (e *badAddressError) Error() string {
return "badly formatted address: " + e.address
}

View File

@ -0,0 +1,50 @@
package webfinder
import (
"errors"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
func Get(c *gin.Context) {
res := c.Query("resource")
if res == "" {
c.String(http.StatusBadRequest, "Missing 'resouce' query parameter in webfinger")
return
}
address, err := getAddress(res)
if err != nil {
c.String(http.StatusBadRequest, err.Error())
}
// TODO find group???
actor, err := atAddress(address)
if err != nil {
if _, ok := err.(*badAddressError); ok {
c.String(http.StatusBadRequest, "Incorrect address format")
return
}
c.String(http.StatusBadRequest, err.Error())
return
}
if actor == nil {
c.String(http.StatusBadRequest, "Account not found")
}
// bytes, err := json.Marshal(actor)
// if err != nil {
// http.Error(w, err.Error(), http.StatusBadRequest)
// return
// }
c.JSON(http.StatusOK, actor)
}
func getAddress(res string) (string, error) {
args := strings.Split(res, ":")
if args[0] != "acct" {
return "", errors.New("Resource didn`t start with acct")
}
return args[1], nil
}

39
main.go Normal file
View File

@ -0,0 +1,39 @@
package main
import (
"git.macaw.me/inhosin/subhub/handlers/groups"
"git.macaw.me/inhosin/subhub/handlers/webfinder"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"net/http"
"os"
)
func init() {
log.SetOutput(os.Stdout)
}
func main() {
r := gin.Default()
r.GET("/.well-known/webfinger", webfinder.Get)
api := r.Group("/api")
{
api.GET("/api/group/:name", groups.Get)
api.POST("/api/group", groups.Create)
}
r.GET("/health", healthHandler)
// Add support for PORT env
port := "8080"
if fromEnv := os.Getenv("PORT"); fromEnv != "" {
port = fromEnv
}
log.WithField("port", port).Info("starting pubcast api server")
log.Fatal(r.Run(":" + port))
}
func healthHandler(c *gin.Context) {
c.String(http.StatusOK, "I`am alive.")
}