add some works
This commit is contained in:
parent
cd9ec0c774
commit
8807e5aaab
1
.gitignore
vendored
1
.gitignore
vendored
@ -29,3 +29,4 @@ tests/db
|
|||||||
|
|
||||||
# vendor dir
|
# vendor dir
|
||||||
vendor/
|
vendor/
|
||||||
|
trash/
|
||||||
|
114
Gopkg.lock
generated
114
Gopkg.lock
generated
@ -1,9 +1,121 @@
|
|||||||
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
# 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]
|
[solve-meta]
|
||||||
analyzer-name = "dep"
|
analyzer-name = "dep"
|
||||||
analyzer-version = 1
|
analyzer-version = 1
|
||||||
input-imports = []
|
input-imports = [
|
||||||
|
"github.com/gin-gonic/gin",
|
||||||
|
"github.com/sirupsen/logrus",
|
||||||
|
]
|
||||||
solver-name = "gps-cdcl"
|
solver-name = "gps-cdcl"
|
||||||
solver-version = 1
|
solver-version = 1
|
||||||
|
152
README.md
152
README.md
@ -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
91
TZ.md
Normal 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
3
activitypub/README.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# ActivityPub
|
||||||
|
|
||||||
|
This package contains functions for handling the ActivityPub spec.
|
145
activitypub/activity.go
Normal file
145
activitypub/activity.go
Normal 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
57
activitypub/person.go
Normal 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
107
activitypub/vocab.go
Normal 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
7
app/account.go
Normal 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
46
app/app.go
Normal 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
6
config/constants.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServerHostName = "subhub.hostname"
|
||||||
|
ServerPort = "subhub.port"
|
||||||
|
)
|
32
data/data.go
Normal file
32
data/data.go
Normal 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
52
data/models/group.go
Normal 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
6
handlers/README.md
Normal 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
41
handlers/groups/get.go
Normal 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
32
handlers/groups/post.go
Normal 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
7
handlers/path.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
// GetFullHostName return string
|
||||||
|
func GetFullHostName() string {
|
||||||
|
// TODO make it at config
|
||||||
|
return "http://" + "localhost" + ":8090"
|
||||||
|
}
|
23
handlers/webfinder/README.md
Normal file
23
handlers/webfinder/README.md
Normal 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.
|
40
handlers/webfinder/address.go
Normal file
40
handlers/webfinder/address.go
Normal 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
|
||||||
|
}
|
26
handlers/webfinder/models.go
Normal file
26
handlers/webfinder/models.go
Normal 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
|
||||||
|
}
|
50
handlers/webfinder/webfinger.go
Normal file
50
handlers/webfinder/webfinger.go
Normal 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
39
main.go
Normal 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.")
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user