This commit is contained in:
Aaron Bieber 2024-04-19 07:04:46 -06:00
commit 5e1adfab60
No known key found for this signature in database
26 changed files with 1059 additions and 0 deletions

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.direnv
*.bak
result
tags
starpub

15
LICENSE Normal file
View File

@ -0,0 +1,15 @@
/*
* Copyright (c) 2024 Aaron Bieber <aaron@bolddaemon.com>
*
* Permission to use, copy, modify, and distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/

32
data/db.go Normal file
View File

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.23.0
package data
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
)
type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
return &Queries{
db: tx,
}
}

47
data/models.go Normal file
View File

@ -0,0 +1,47 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.23.0
package data
import (
"github.com/jackc/pgx/v5/pgtype"
)
type Category struct {
ID int32 `json:"id"`
UserID int32 `json:"user_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Name string `json:"name"`
Descr string `json:"descr"`
}
type Entry struct {
ID int32 `json:"id"`
UserID int32 `json:"user_id"`
Pubic pgtype.Bool `json:"pubic"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Title string `json:"title"`
Descr string `json:"descr"`
Signature string `json:"signature"`
}
type EntryCategory struct {
UserID int32 `json:"user_id"`
EntryID int32 `json:"entry_id"`
CategoryID int32 `json:"category_id"`
}
type User struct {
ID int32 `json:"id"`
Uid pgtype.UUID `json:"uid"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
RealName string `json:"real_name"`
Username string `json:"username"`
Hash string `json:"hash"`
Email string `json:"email"`
Pubkey []byte `json:"pubkey"`
}

308
data/queries.sql.go Normal file
View File

@ -0,0 +1,308 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.23.0
// source: queries.sql
package data
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const createEntry = `-- name: CreateEntry :one
INSERT INTO entries (
user_id, title, descr
) VALUES (
$1, $2, $3
)
RETURNING id, created_at, to_tsvector(descr)
`
type CreateEntryParams struct {
UserID int32 `json:"user_id"`
Title string `json:"title"`
Descr string `json:"descr"`
}
type CreateEntryRow struct {
ID int32 `json:"id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
ToTsvector interface{} `json:"to_tsvector"`
}
func (q *Queries) CreateEntry(ctx context.Context, arg CreateEntryParams) (CreateEntryRow, error) {
row := q.db.QueryRow(ctx, createEntry, arg.UserID, arg.Title, arg.Descr)
var i CreateEntryRow
err := row.Scan(&i.ID, &i.CreatedAt, &i.ToTsvector)
return i, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO users (
real_name, username, pubkey
) VALUES (
$1, $2, $3
)
RETURNING id, username, pubkey
`
type CreateUserParams struct {
RealName string `json:"real_name"`
Username string `json:"username"`
Pubkey []byte `json:"pubkey"`
}
type CreateUserRow struct {
ID int32 `json:"id"`
Username string `json:"username"`
Pubkey []byte `json:"pubkey"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
row := q.db.QueryRow(ctx, createUser, arg.RealName, arg.Username, arg.Pubkey)
var i CreateUserRow
err := row.Scan(&i.ID, &i.Username, &i.Pubkey)
return i, err
}
const getEntryCategories = `-- name: GetEntryCategories :many
SELECT
id, user_id, created_at, updated_at, name, descr
FROM
categories
WHERE
id IN (
SELECT
category_id
FROM
entry_categories a
WHERE
a.entry_id = $1
AND a.user_id = $2
)
`
type GetEntryCategoriesParams struct {
EntryID int32 `json:"entry_id"`
UserID int32 `json:"user_id"`
}
func (q *Queries) GetEntryCategories(ctx context.Context, arg GetEntryCategoriesParams) ([]Category, error) {
rows, err := q.db.Query(ctx, getEntryCategories, arg.EntryID, arg.UserID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Category{}
for rows.Next() {
var i Category
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Descr,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUser = `-- name: GetUser :one
SELECT
id, uid, created_at, updated_at, real_name, username, hash, email, pubkey
FROM
users
WHERE
id = $1
LIMIT
1
`
func (q *Queries) GetUser(ctx context.Context, id int32) (User, error) {
row := q.db.QueryRow(ctx, getUser, id)
var i User
err := row.Scan(
&i.ID,
&i.Uid,
&i.CreatedAt,
&i.UpdatedAt,
&i.RealName,
&i.Username,
&i.Hash,
&i.Email,
&i.Pubkey,
)
return i, err
}
const getUserByName = `-- name: GetUserByName :one
SELECT
id, uid, created_at, updated_at, real_name, username, hash, email, pubkey
FROM
users
WHERE
username = $1
LIMIT
1
`
func (q *Queries) GetUserByName(ctx context.Context, username string) (User, error) {
row := q.db.QueryRow(ctx, getUserByName, username)
var i User
err := row.Scan(
&i.ID,
&i.Uid,
&i.CreatedAt,
&i.UpdatedAt,
&i.RealName,
&i.Username,
&i.Hash,
&i.Email,
&i.Pubkey,
)
return i, err
}
const getUserCategories = `-- name: GetUserCategories :many
SELECT
id, user_id, created_at, updated_at, name, descr
FROM
categories
WHERE
user_id = $1
`
func (q *Queries) GetUserCategories(ctx context.Context, userID int32) ([]Category, error) {
rows, err := q.db.Query(ctx, getUserCategories, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Category{}
for rows.Next() {
var i Category
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.CreatedAt,
&i.UpdatedAt,
&i.Name,
&i.Descr,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUserEntries = `-- name: GetUserEntries :many
SELECT
id, user_id, pubic, created_at, updated_at, title, descr, signature
FROM
entries
WHERE
user_id = $1
`
func (q *Queries) GetUserEntries(ctx context.Context, userID int32) ([]Entry, error) {
rows, err := q.db.Query(ctx, getUserEntries, userID)
if err != nil {
return nil, err
}
defer rows.Close()
items := []Entry{}
for rows.Next() {
var i Entry
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.Pubic,
&i.CreatedAt,
&i.UpdatedAt,
&i.Title,
&i.Descr,
&i.Signature,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const similarEntries = `-- name: SimilarEntries :many
SELECT
id,
similarity(descr, $2) AS similarity,
ts_headline(
'english',
descr,
q,
'StartSel = <b>, StopSel = </b>'
) :: text AS headline,
title
FROM
entries,
to_tsquery($2) q
WHERE
user_id = $1
AND similarity(descr, $2) > 0.0
AND similarity(descr, $2) < 1.0
ORDER BY
similarity DESC
LIMIT
10
`
type SimilarEntriesParams struct {
UserID int32 `json:"user_id"`
Similarity string `json:"similarity"`
}
type SimilarEntriesRow struct {
ID int32 `json:"id"`
Similarity float32 `json:"similarity"`
Headline string `json:"headline"`
Title string `json:"title"`
}
func (q *Queries) SimilarEntries(ctx context.Context, arg SimilarEntriesParams) ([]SimilarEntriesRow, error) {
rows, err := q.db.Query(ctx, similarEntries, arg.UserID, arg.Similarity)
if err != nil {
return nil, err
}
defer rows.Close()
items := []SimilarEntriesRow{}
for rows.Next() {
var i SimilarEntriesRow
if err := rows.Scan(
&i.ID,
&i.Similarity,
&i.Headline,
&i.Title,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}

27
extension/background.js Normal file
View File

@ -0,0 +1,27 @@
import * as Starpub from "./starpub.js";
document.addEventListener("DOMContentLoaded", function () {
let starKey = new Starpub();
browser.browserAction.onClicked.addListener(function () {
let maybeKey = starKey.getKey();
maybeKey.then((_) => {
let tab = browser.tabs.query({ active: true, currentWindow: true });
tab.then(function (a) {
const t = a[0];
const st = starKey.sign({
url: t.url,
title: t.title,
});
st.then((p) => {
const vfy = Coze.Verify(p, starKey.pubKey);
vfy.then((v) => {
console.log("signature verified: ", v);
console.log(t.url, t.title);
console.log(starKey.pubKey);
});
});
});
});
});
});

2
extension/coze.min.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
extension/icons/star-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

30
extension/manifest.json Normal file
View File

@ -0,0 +1,30 @@
{
"manifest_version": 2,
"name": "StarPub",
"version": "1.0",
"browser_specific_settings": {
"gecko": {
"id": "starpub@starpuber"
}
},
"permissions": ["tabs", "storage"],
"description": "Star anything",
"icons": {
"48": "icons/star-48.png"
},
"browser_action": {
"default_icon": "icons/star-48.png",
"default_title": "Star it!",
"default_popup": "popup.html"
},
"background": {
"scripts": ["coze.min.js", "background.js"],
"type": "module"
}
}

20
extension/popup.html Normal file
View File

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div>
<div>
<input id="title" type="text"/>
<br />
<textarea id="description"></textarea>
<button>Add Star</button>
</div>
</div>
<script type="module" src="popup.js"></script>
</body>
</html>

28
extension/popup.js Normal file
View File

@ -0,0 +1,28 @@
"use strict";
import * as Starpub from "./starpub.min.js";
let starKey = new Starpub();
browser.browserAction.onClicked.addListener(function () {
let maybeKey = starKey.getKey();
maybeKey.then((_) => {
let tab = browser.tabs.query({ active: true, currentWindow: true });
tab.then(function (a) {
const t = a[0];
const st = starKey.sign({
url: t.url,
title: t.title,
});
st.then((p) => {
const vfy = Coze.Verify(p, starKey.pubKey);
vfy.then((v) => {
let titleInp = document.getElementById("title");
let descInp = document.getElementById("description");
console.log("signature verified: ", v, t);
titleInp.value = t.title;
});
});
});
});
});

20
extension/settings.html Normal file
View File

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
<div>
<div>
<input id="title" type="text"/>
<br />
<textarea id="description"></textarea>
<button>Add Star</button>
</div>
</div>
<script type="module" src="popup.js"></script>
</body>
</html>

46
extension/starpub.js Normal file
View File

@ -0,0 +1,46 @@
"use strict";
import * as Coze from "./coze.min.js";
export { Starpub };
class Starpub {
async getKey() {
let key = await browser.storage.local.get("starKey");
if (Object.keys(key).length < 1) {
this.key = await Coze.NewKey("ES512");
this.key.kid = "My StarPub Coze Key";
await browser.storage.local.set({ starKey: this.key });
} else {
this.key = key.starKey;
}
this.thumb = await Coze.Thumbprint(this.key);
const { d, ...pkRest } = this.key;
this.pubKey = pkRest;
}
async sign(starObj) {
return await Coze.SignCozeRaw(
{
pay: {
url: starObj.url,
title: starObj.title,
typ: "starpub/msg/create",
iat: Math.floor(Date.now() / 1000),
alg: this.key.alg,
tmb: this.key.tmb,
},
},
this.key,
);
}
async verify() {
return false;
}
async publish() {}
}

BIN
extension/starpub.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

26
flake.lock Normal file
View File

@ -0,0 +1,26 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1711668574,
"narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-23.11",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

57
flake.nix Normal file
View File

@ -0,0 +1,57 @@
{
description = "starpub: star anything";
inputs.nixpkgs.url = "nixpkgs/nixos-23.11";
outputs =
{ self
, nixpkgs
,
}:
let
supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; });
in
{
overlay = _: prev: { inherit (self.packages.${prev.system}) starpub; };
packages = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
in
{
starpub = pkgs.buildGoModule {
pname = "starpub";
version = "v0.0.0";
src = ./.;
vendorHash = pkgs.lib.fakeSha256;
};
});
defaultPackage = forAllSystems (system: self.packages.${system}.starpub);
devShells = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
in
{
default = pkgs.mkShell {
shellHook = ''
PS1='\u@\h:\@; '
echo "Go `${pkgs.go}/bin/go version`"
'';
nativeBuildInputs = with pkgs; [
git
go
gopls
go-tools
sqlc
(pkgs.callPackage (import ./ogen.nix) { })
(postgresql.withPackages (p: [ p.postgis ]))
];
};
});
};
}

4
generate.go Normal file
View File

@ -0,0 +1,4 @@
package main
//go:generate ogen --target petstore --clean openapi.yaml
//go:generate sqlc generate

37
go.mod Normal file
View File

@ -0,0 +1,37 @@
module suah.dev/starpub
go 1.22.1
require (
github.com/go-faster/errors v0.7.1
github.com/go-faster/jx v1.1.0
github.com/jackc/pgx/v5 v5.5.5
github.com/ogen-go/ogen v1.0.0
go.opentelemetry.io/otel v1.25.0
go.opentelemetry.io/otel/metric v1.25.0
go.opentelemetry.io/otel/trace v1.25.0
)
require (
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-faster/yaml v0.4.6 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/segmentio/asm v1.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/crypto v0.21.0 // indirect
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

88
go.sum Normal file
View File

@ -0,0 +1,88 @@
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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg=
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
github.com/go-faster/jx v1.1.0 h1:ZsW3wD+snOdmTDy9eIVgQdjUpXRRV4rqW8NS3t+20bg=
github.com/go-faster/jx v1.1.0/go.mod h1:vKDNikrKoyUmpzaJ0OkIkRQClNHFX/nF3dnTJZb3skg=
github.com/go-faster/yaml v0.4.6 h1:lOK/EhI04gCpPgPhgt0bChS6bvw7G3WwI8xxVe0sw9I=
github.com/go-faster/yaml v0.4.6/go.mod h1:390dRIvV4zbnO7qC9FGo6YYutc+wyyUSHBgbXL52eXk=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/ogen-go/ogen v1.0.0 h1:n1hkgOnLtA1Xn369KAzJhqzphQzNo/wAI82NIaFQNXA=
github.com/ogen-go/ogen v1.0.0/go.mod h1:NFn616zR+/DPsq8rPoezaHlhKcNQzlYfo5gUieW8utI=
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/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=

57
main.go Normal file
View File

@ -0,0 +1,57 @@
package main
import (
"context"
"log"
"net"
"net/http"
"github.com/jackc/pgx/v5"
"suah.dev/starpub/data"
)
var ()
func logger(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s\n", r.URL.Path)
f(w, r)
}
}
func makeHandler(q *data.Queries, ctx context.Context) func(w http.ResponseWriter, r *http.Request) {
return nil
}
func main() {
pd, err := pgx.Connect(
context.Background(),
"host=localhost dbname=postgres sslmode=disable password=''",
)
if err != nil {
log.Fatal(err)
}
base := data.New(pd)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
mux := http.NewServeMux()
mux.HandleFunc("/user/auth", logger(makeHandler(base, ctx)))
mux.HandleFunc("POST /stars", logger(nil))
mux.HandleFunc("DELETE /stars", logger(nil))
mux.HandleFunc("GET /{user}", logger(nil))
mux.HandleFunc("GET /{user}.rss", logger(nil))
s := http.Server{
Handler: mux,
}
lis, err := net.Listen("tcp", ":8080")
if err != nil {
log.Fatal(err)
}
s.Serve(lis)
}

26
ogen.nix Normal file
View File

@ -0,0 +1,26 @@
{ lib
, buildGoModule
, fetchFromGitHub
, ...
}:
with lib;
buildGoModule rec {
pname = "ogen";
version = "1.0.0";
src = fetchFromGitHub {
owner = "ogen-go";
repo = pname;
rev = "v${version}";
sha256 = "sha256-khUY8PQ12p0slleidysZiGWyZNX3XCxwD55lfRDCwew=";
};
vendorHash = "sha256-kHfp77jKPBt+EKAc52nXdqR4TJ3OVj7mqo4vfv2XelQ=";
meta = {
description = "OpenAPI v3 code generator for go";
homepage = "https://github.com/ogen-go/ogen";
license = licenses.asl20;
maintainers = with maintainers; [ qbit ];
};
}

33
openapi.yaml Normal file
View File

@ -0,0 +1,33 @@
openapi: 3.0.0
info:
title: Starpub API
description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML.
version: 1.0.0
servers:
- url: http://api.example.com/v1
description: Optional server description, e.g. Main (production) server
- url: http://staging-api.example.com
description: Optional server description, e.g. Internal staging server for testing
paths:
'/{user}':
get:
summary: Returns a list of users.
description: Optional extended description in CommonMark or HTML.
parameters:
- name: user
in: path
required: true
description: username
schema:
type: string
responses:
"200": # status code
description: A JSON array of user names
content:
application/json:
schema:
type: array
items:
type: string

90
queries.sql Normal file
View File

@ -0,0 +1,90 @@
-- name: GetUserCategories :many
SELECT
*
FROM
categories
WHERE
user_id = $1;
-- name: GetUserEntries :many
SELECT
*
FROM
entries
WHERE
user_id = $1;
-- name: CreateUser :one
INSERT INTO users (
real_name, username, pubkey
) VALUES (
$1, $2, $3
)
RETURNING id, username, pubkey;
-- name: SimilarEntries :many
SELECT
id,
similarity(descr, $2) AS similarity,
ts_headline(
'english',
descr,
q,
'StartSel = <b>, StopSel = </b>'
) :: text AS headline,
title
FROM
entries,
to_tsquery($2) q
WHERE
user_id = $1
AND similarity(descr, $2) > 0.0
AND similarity(descr, $2) < 1.0
ORDER BY
similarity DESC
LIMIT
10;
-- name: GetEntryCategories :many
SELECT
*
FROM
categories
WHERE
id IN (
SELECT
category_id
FROM
entry_categories a
WHERE
a.entry_id = $1
AND a.user_id = $2
);
-- name: GetUser :one
SELECT
*
FROM
users
WHERE
id = $1
LIMIT
1;
-- name: GetUserByName :one
SELECT
*
FROM
users
WHERE
username = $1
LIMIT
1;
-- name: CreateEntry :one
INSERT INTO entries (
user_id, title, descr
) VALUES (
$1, $2, $3
)
RETURNING id, created_at, to_tsvector(descr);

48
schema.sql Normal file
View File

@ -0,0 +1,48 @@
CREATE EXTENSION IF NOT EXISTS pg_trgm;
CREATE EXTENSION IF NOT EXISTS pgcrypto;
DROP Table if exists entry_categories;
drop table if exists categories;
drop table if exists entries;
drop table if exists users;
CREATE TABLE users (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
uid UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
created_at timestamp NOT NULL DEFAULT NOW(),
updated_at timestamp,
real_name text NOT NULL,
username text NOT NULL UNIQUE,
hash text NOT NULL,
email text NOT NULL,
pubkey jsonb NOT NULL
);
CREATE TABLE categories (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
created_at timestamp NOT NULL DEFAULT NOW(),
updated_at timestamp,
name text NOT NULL,
descr text NOT NULL
);
CREATE TABLE entries (
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
pubic bool DEFAULT TRUE,
created_at timestamp NOT NULL DEFAULT NOW(),
updated_at timestamp,
title text NOT NULL DEFAULT '',
descr text NOT NULL DEFAULT '',
signature text NOT NULL
);
CREATE TABLE entry_categories (
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
entry_id INTEGER NOT NULL REFERENCES entries (id) ON DELETE CASCADE,
category_id INTEGER NOT NULL REFERENCES categories (id) ON DELETE CASCADE
);
CREATE INDEX entry_trgm_idx ON entries USING gist (title, descr gist_trgm_ops);

12
sqlc.yaml Normal file
View File

@ -0,0 +1,12 @@
version: 2
sql:
- engine: "postgresql"
schema: "schema.sql"
queries: "queries.sql"
gen:
go:
sql_package: "pgx/v5"
package: "data"
out: "data"
emit_json_tags: true
emit_empty_slices: true