Compare commits

...

No commits in common. "main" and "BKCH" have entirely different histories.
main ... BKCH

49 changed files with 2619 additions and 425 deletions

43
.gitignore vendored
View file

@ -1,41 +1,2 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
justfile
static/*

View file

@ -1,40 +0,0 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
# Structure
Store on local machine and use a middleware to process (compress) the file and facilitate storage.

100
admin/create/index.html Normal file
View file

@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIMING NewUser</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
</head>
<body class="m-0 grid place-center w-dvw h-dvh bg-zinc-800 text-zinc-900">
<script>
let key = ""
document.addEventListener("DOMContentLoaded", function () {
let username = document.getElementById("username")
let password = document.getElementById("password")
let gen_img = document.getElementById("gen")
let img = document.getElementById("totp_img")
let create = document.getElementById("create")
const get_img = () => {
console.log(username.value)
if (username.value.length == 0) {
alert("缺少名稱")
}
gen_img.disabled = true
fetch(`/api/admin/new_totp?name=${encodeURIComponent(username.value)}`).then(async resp => {
const resp_obj = await resp.json()
key = resp_obj["key"]
img.src = "data:image/png;base64, " + resp_obj["img"]
}).finally(() => {
gen_img.disabled = false
})
}
gen_img.onclick = get_img;
const create_func = () => {
if (username.value.length == 0) {
alert("username required")
return
}
if (password.value.length < 8) {
alert("password must contain over 8 characters")
return
}
if (key.length == 0) {
alert("Didn't set a totp code")
return
}
fetch("/api/admin/create", {
body: JSON.stringify(
{
"username": username.value
, "password": password.value
, "totp_secret": key
}),
headers: {"Content-Type": "application/json"},
method: "POST"
}).then(resp => {
if (resp.status == 201) {
alert("created")
username.value = ""
password.value = ""
key = ""
img.src = ""
} else {
alert(`Not succeeded, status: ${resp.statusText}`)
}
}).catch(err => {
alert(err)
})
}
create.onclick = create_func
})
</script>
<div
class="drop-shadow-lg flex flex-col box-border py-5 px-3 rounded-xl bg-zinc-400 m-auto h-fit w-10/12 md:w-5/12 lg:w-1/4 gap-5">
<div class="flex flex-col gap-1">
<label>使用者名稱</label>
<input type="text" name="username"
class="focus:border-zinc-600 outline-zinc-500 pl-2 border-2 rounded-lg border-zinc-800 caret-zinc-800 outline-none"
id="username" />
</div>
<div class="flex flex-col gap-1">
<label>密碼</label>
<input type="text" name="password"
class="focus:border-zinc-600 outline-zinc-500 pl-2 border-2 rounded-lg border-zinc-800 caret-zinc-800 outline-none"
id="password" />
</div>
<div class="flex flex-col gap-1">
<button id="gen">產生TOTP QrCODE</button>
<img src="" id="totp_img" class="max-w-30 max-h-30 mx-auto" />
</div>
<button class="border-2 border-slate-700 w-50 rounded-xl mx-auto" id="create">確認</button>
</div>
</body>
</html>

127
admin/login/index.html Normal file
View file

@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NIMING Login</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://unpkg.com/htmx.org@2.0.4"
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
crossorigin="anonymous"></script>
</head>
<body class="grid place-center w-dvw h-dvh bg-zinc-900">
<script>
const totp_length = 6;
document.addEventListener("DOMContentLoaded", function () {
let totp_cell = document.getElementById("totp_cell");
let totp_container = document.getElementById("totp");
for (let i = 0; i < totp_length; i++) {
totp_cell.content.firstElementChild.dataset.indexNumber = i
totp_cell.content.firstElementChild.name = `totp-${i}`
let node = document.importNode(totp_cell.content, true)
totp_container.appendChild(node);
}
const keyup_ev = (ev) => {
if (ev.keyCode <= 0x39 && ev.keyCode >= 0x30) {
ev.target.value = ev.key
if (ev.target.nextElementSibling) {
ev.target.nextElementSibling.focus()
}
}
if (ev.key = "a" && ev.ctrlKey) {
console.log("Select all")
for (let i = 1; i <= totp_length; i++) {
totp_container.children[i].select()
}
}
if (ev.key == "Backspace" && ev.target.dataset.indexNumber > 0) {
const previous = totp_container.querySelector(`[data-index-number='${ev.target.dataset.indexNumber - 1}']`)
previous.focus()
}
}
const paste_input_ev = (ev) => {
if (ev.inputType == "insertFromPaste") {
let data = ev.data
console.log(data)
for (let i = 1; i <= totp_length; i++) {
totp_container.children[i].value = data.slice(0, 1)
data = data.slice(1, data.length)
}
}
}
for (const cell of totp_container.children) {
if (cell instanceof HTMLInputElement) {
cell.addEventListener("keyup", keyup_ev)
cell.addEventListener("input", paste_input_ev)
}
}
const form_onsubmit = (ev) => {
let form = new FormData(ev.target)
let miss = form.entries().find(p => p[1] == "")
if (miss) {
alert(`Missing ${miss[0]}`)
ev.preventDefault()
}
console.log(form)
}
const form_formdata = (ev) => {
const data = ev.formData
let totp = new Array(totp_length)
for (const [k, v] of data.entries()) {
if (k.match(/totp-\d/g)) {
totp[parseInt(k.split("-")[1])] = v
}
}
for (let i = 0; i < totp_length; i++) {
data.delete(`totp-${i}`)
}
data.set("totp_code", totp.join(""))
}
const form = document.getElementById("login_form")
form.addEventListener("submit", form_onsubmit)
form.addEventListener("formdata", form_formdata)
});
</script>
<form id="login_form"
class="m-auto h-fit w-10/12 md:w-5/12 lg:w-1/4 bg-zinc-400 drop-shadow-lg flex flex-col px-10 box-border py-5 gap-10 rounded-xl"
action="/admin/login" method="post">
<h1 class="text-3xl text-center">中工匿名管理</h1>
<div class="flex flex-col gap-2">
<label for="username">名稱</label>
<input name="name" id="username" type="text"
class="block rounded outline-none ring-3 border-box px-3" />
</div>
<div class="flex flex-col gap-2">
<label for="password">密碼</label>
<input name="password" id="password" type="password"
class="block rounded outline-none ring-3 border-box px-3" />
</div>
<div class="flex flex-col gap-2">
<label for="totp">TOTP</label>
<div class="flex flex-row gap-3 w-full px-auto justify-center box-border" id="totp">
<template id="totp_cell">
<input type="text" inputmode="numeric" pattern="[0-9]{1}"
class="block rounded outline-none ring-2 border-box w-7 h-7 text-center totp"
data-index-number="0" maxlength="1" minlength="1" />
</template>
</div>
</div>
<button type="submit"
class="w-fit px-5 py-1 mx-auto bg-sky-500 rounded-lg drop-shadow-cyan-500/50 drop-shadow-lg">確認</button>
</form>
</body>
</html>

163
admin/panel/api.js Normal file
View file

@ -0,0 +1,163 @@
class Post {
content;
signing;
post_at;
media;
id;
constructor(id, content, signing, post_at, media) {
this.id = id;
this.content = content;
this.signing = signing;
this.post_at = post_at;
this.media = media;
}
verify(check) {
this.check = check;
}
get verify_blk() {
return {
post: this.id,
check: this.check,
};
}
}
const Fetch = async () => {
let result = await fetch("/api/admin/fetch_post", {
credentials: "include",
});
return (await result.json()).map((e) => {
return new Post(e.id, e.content, e.signing, e.post_at, e.enclosure);
});
};
let sending = [];
document.addEventListener("DOMContentLoaded", function () {
let verix_container = document.getElementById("verix_container");
const verify = (id, obj, ck) => {
obj.verify(ck);
sending.push(obj);
let child = document.getElementById(`post-${id}`);
verix_container.removeChild(child);
};
let hv = document.getElementById("hv");
const disable_display = () => {
hv.classList.add("hidden");
};
hv.onclick = disable_display;
const hover_display_img = (src) => {
hv.classList.remove("hidden");
console.log(hv.children);
hv.children[0].src = src;
};
Fetch()
.then((e) => {
if (e.length == 0) {
let nothing = document.createElement("div");
nothing.appendChild(document.createTextNode("Nothing!!!"));
nothing.className = "text-slate-300 text-3xl mx-auto";
verix_container.appendChild(nothing);
} else
e.forEach((x) => {
let post = document.createElement("div");
post.className =
"rounded bg-zinc-800 w-full h-fit text-slate-200 px-2 relative break-all";
let post_content = document.createElement("p");
post_content.className = "h-fit text-md min-h-20 mr-[30px]";
post_content.appendChild(document.createTextNode(x.content));
post.appendChild(post_content);
let media = document.createElement("div");
media.className = "w-full flex flex-row gap-2 h-fit overflow-x-auto";
post.appendChild(media);
if (x.media) {
x.media.forEach((src) => {
let media_cell = document.createElement("img");
media_cell.className = "h-30 w-auto my-auto";
media_cell.src = `/static/${src}`;
media_cell.onclick = () => {
hover_display_img(media_cell.src);
};
media.appendChild(media_cell);
});
}
let btm_bar = document.createElement("div");
let msg = document.createElement("p");
msg.className = "text-sm border-t-3";
msg.appendChild(
document.createTextNode(
`id: ${x.id}, ${x.signing ? "sign: " + x.signing + ", " : ""}post_at: ${new Date(x.post_at).toDateString()}`,
),
);
btm_bar.appendChild(msg);
post.appendChild(btm_bar);
let right_bar = document.createElement("div");
right_bar.classList =
"h-10 w-fit absolute right-0 top-0 flex flex-col gap-2 text-lg";
post.appendChild(right_bar);
let btn_cls = "rounded-2xl w-[30px] h-[30px] bg-zinc-400";
let revoke = document.createElement("button");
revoke.className = btn_cls;
revoke.onclick = () => {
verify(x.id, x, false);
};
revoke.appendChild(document.createTextNode("X"));
let issue = document.createElement("button");
issue.className = btn_cls;
issue.onclick = () => {
verify(x.id, x, true);
};
issue.appendChild(document.createTextNode("Y"));
right_bar.appendChild(revoke);
right_bar.appendChild(issue);
post.id = `post-${x.id}`;
verix_container.appendChild(post);
});
})
.catch((err) => {
alert(err);
});
const sendbtn = document.getElementById("send");
const send = () => {
sendbtn.disabled = true;
sendbtn.classList.remove("text-zinc-200");
sendbtn.classList.add("text-zinc-400");
if (sending.length > 0) {
fetch("/api/admin/verify_post", {
body: JSON.stringify({ choice: sending.map((e) => e.verify_blk) }),
headers: { "Content-Type": "application/json" },
credentials: "include",
method: "PUT",
})
.then(async (resp) => {
if (resp.status == 200) {
sending = [];
alert("成功");
} else {
alert(`Err: ${resp.statusText}, ${await resp.text()}`);
}
})
.catch((err) => alert(err))
.finally(() => {
sendbtn.disabled = false;
sendbtn.classList.add("text-zinc-200");
sendbtn.classList.remove("text-zinc-400");
});
} else {
alert("Nothing");
sendbtn.classList.add("text-zinc-200");
sendbtn.classList.remove("text-zinc-400");
sendbtn.disabled = false;
}
};
sendbtn.onclick = send;
});

29
admin/panel/index.html Normal file
View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Panel</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://unpkg.com/htmx.org@2.0.4"
integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
crossorigin="anonymous"></script>
<script src="/admin/panel/api.js"></script>
</head>
<body class="m-0 w-dvw h-dvh bg-zinc-800 grid place-center">
<div class="mx-auto h-dvh w-[90dvw] lg:w-[70dvw]">
<button class="h-1/20 w-full text-zinc-200 border-2 rounded my-[25px]" id="send">Send</button>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2 bg-zinc-700 px-2 overflow-auto h-[calc(95dvh-50px)] box-border"
id="verix_container">
</div>
</div>
<div class="hidden fixed top-0 left-0 w-dvw h-dvh z-10 backdrop-brightness-30" id="hv">
<img src="" class="h-70dvh lg:w-[40dvw] fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-20"
alt="img" />
</div>
</body>
</html>

63
adminadd.go Normal file
View file

@ -0,0 +1,63 @@
package main
import (
"context"
"encoding/base64"
"fmt"
"log"
"os"
"time"
"github.com/jackc/pgx/v5"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/scrypt"
"nim.jasinco.work/app/nimdb"
)
func main() {
pgurl := os.Getenv("POSTGRES_URL")
conn, err := pgx.Connect(context.Background(), pgurl)
if err != nil {
log.Fatalln(err.Error())
}
salt := os.Getenv("SALT")
db := nimdb.New(conn)
tx, err := conn.Begin(context.Background())
if err != nil {
log.Fatalln(err.Error())
}
qtx := db.WithTx(tx)
defer tx.Rollback(context.Background())
fmt.Print("UserName and password (split by space): ")
var name, password string
_, err = fmt.Scanf("%s %s", &name, &password)
if err != nil {
log.Fatal(err)
}
key, err := totp.Generate(totp.GenerateOpts{Issuer: "TCIVS_NIMING", AccountName: name})
if err != nil {
log.Fatalln(err)
}
secret := key.Secret()
log.Println(secret, key.Issuer(), key.AccountName())
fmt.Print("Verify TOTP Code: ")
var code string
_, err = fmt.Scanf("%s", &code)
if !totp.Validate(code, secret) {
gen, err := totp.GenerateCode(secret, time.Now())
if err != nil {
log.Fatalln("Validation not succed, can't gen code, err:", err.Error())
}
log.Fatalln("Velidation not succed, CODE should be: ", gen)
}
hashed, err := scrypt.Key([]byte(password), []byte(salt), 32768, 8, 1, 32)
if err != nil {
log.Fatalln(err.Error())
}
qtx.AdminCreateAccount(context.Background(), nimdb.AdminCreateAccountParams{Username: name, Password: base64.StdEncoding.EncodeToString(hashed), Totp: secret})
tx.Commit(context.Background())
}

BIN
bun.lockb

Binary file not shown.

39
go.mod Normal file
View file

@ -0,0 +1,39 @@
module nim.jasinco.work/app
go 1.24.3
require (
github.com/gofiber/contrib/jwt v1.1.2
github.com/gofiber/fiber/v2 v2.52.8
github.com/golang-jwt/jwt/v5 v5.2.2
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.7.5
github.com/pquerna/otp v1.5.0
golang.org/x/crypto v0.38.0
google.golang.org/grpc v1.72.2
google.golang.org/protobuf v1.36.6
)
require (
github.com/MicahParks/keyfunc/v2 v2.1.0 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/tinylib/msgp v1.2.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.62.0 // indirect
golang.org/x/net v0.40.0 // indirect
golang.org/x/sync v0.14.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
)

96
go.sum Normal file
View file

@ -0,0 +1,96 @@
github.com/MicahParks/keyfunc/v2 v2.1.0 h1:6ZXKb9Rp6qp1bDbJefnG7cTH8yMN1IC/4nf+GVjO99k=
github.com/MicahParks/keyfunc/v2 v2.1.0/go.mod h1:rW42fi+xgLJ2FRRXAfNx9ZA8WpD4OeE/yHVMteCkw9k=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
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/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/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/gofiber/contrib/jwt v1.1.2 h1:GmWnOqT4A15EkA8IPXwSpvNUXZR4u5SMj+geBmyLAjs=
github.com/gofiber/contrib/jwt v1.1.2/go.mod h1:CpIwrkUQ3Q6IP8y9n3f0wP9bOnSKx39EDp2fBVgMFVk=
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
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/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c h1:dAMKvw0MlJT1GshSTtih8C2gDs04w8dReiOGXrGLNoY=
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
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/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.62.0 h1:8dKRBX/y2rCzyc6903Zu1+3qN0H/d2MsxPPmVNamiH0=
github.com/valyala/fasthttp v1.62.0/go.mod h1:FCINgr4GKdKqV8Q0xv8b+UxPV+H/O5nNFo3D+r54Htg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8=
google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

211
igapi/interface.pb.go Normal file
View file

@ -0,0 +1,211 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.6
// protoc v6.31.0
// source: igapi/interface.proto
package igapi
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Request struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Igid string `protobuf:"bytes,2,opt,name=igid,proto3" json:"igid,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Request) Reset() {
*x = Request{}
mi := &file_igapi_interface_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_igapi_interface_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_igapi_interface_proto_rawDescGZIP(), []int{0}
}
func (x *Request) GetId() int64 {
if x != nil {
return x.Id
}
return 0
}
func (x *Request) GetIgid() string {
if x != nil {
return x.Igid
}
return ""
}
type Reply struct {
state protoimpl.MessageState `protogen:"open.v1"`
Err int64 `protobuf:"varint,1,opt,name=err,proto3" json:"err,omitempty"`
Result map[string]string `protobuf:"bytes,2,rep,name=result,proto3" json:"result,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Reply) Reset() {
*x = Reply{}
mi := &file_igapi_interface_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Reply) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Reply) ProtoMessage() {}
func (x *Reply) ProtoReflect() protoreflect.Message {
mi := &file_igapi_interface_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Reply.ProtoReflect.Descriptor instead.
func (*Reply) Descriptor() ([]byte, []int) {
return file_igapi_interface_proto_rawDescGZIP(), []int{1}
}
func (x *Reply) GetErr() int64 {
if x != nil {
return x.Err
}
return 0
}
func (x *Reply) GetResult() map[string]string {
if x != nil {
return x.Result
}
return nil
}
var File_igapi_interface_proto protoreflect.FileDescriptor
const file_igapi_interface_proto_rawDesc = "" +
"\n" +
"\x15igapi/interface.proto\"-\n" +
"\aRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x12\n" +
"\x04igid\x18\x02 \x01(\tR\x04igid\"\x80\x01\n" +
"\x05Reply\x12\x10\n" +
"\x03err\x18\x01 \x01(\x03R\x03err\x12*\n" +
"\x06result\x18\x02 \x03(\v2\x12.Reply.ResultEntryR\x06result\x1a9\n" +
"\vResultEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x012\xbf\x01\n" +
"\x05IGAPI\x12\x1b\n" +
"\x05login\x12\b.Request\x1a\x06.Reply\"\x00\x12\"\n" +
"\faccount_info\x12\b.Request\x1a\x06.Reply\"\x00\x12\x1c\n" +
"\x06upload\x12\b.Request\x1a\x06.Reply\"\x00\x12\x1c\n" +
"\x06delete\x12\b.Request\x1a\x06.Reply\"\x00\x12\x1b\n" +
"\x05queue\x12\b.Request\x1a\x06.Reply\"\x00\x12\x1c\n" +
"\x06search\x12\b.Request\x1a\x06.Reply\"\x00B\x1cZ\x1anim.jasinco.work/app/igapib\x06proto3"
var (
file_igapi_interface_proto_rawDescOnce sync.Once
file_igapi_interface_proto_rawDescData []byte
)
func file_igapi_interface_proto_rawDescGZIP() []byte {
file_igapi_interface_proto_rawDescOnce.Do(func() {
file_igapi_interface_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_igapi_interface_proto_rawDesc), len(file_igapi_interface_proto_rawDesc)))
})
return file_igapi_interface_proto_rawDescData
}
var file_igapi_interface_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
var file_igapi_interface_proto_goTypes = []any{
(*Request)(nil), // 0: Request
(*Reply)(nil), // 1: Reply
nil, // 2: Reply.ResultEntry
}
var file_igapi_interface_proto_depIdxs = []int32{
2, // 0: Reply.result:type_name -> Reply.ResultEntry
0, // 1: IGAPI.login:input_type -> Request
0, // 2: IGAPI.account_info:input_type -> Request
0, // 3: IGAPI.upload:input_type -> Request
0, // 4: IGAPI.delete:input_type -> Request
0, // 5: IGAPI.queue:input_type -> Request
0, // 6: IGAPI.search:input_type -> Request
1, // 7: IGAPI.login:output_type -> Reply
1, // 8: IGAPI.account_info:output_type -> Reply
1, // 9: IGAPI.upload:output_type -> Reply
1, // 10: IGAPI.delete:output_type -> Reply
1, // 11: IGAPI.queue:output_type -> Reply
1, // 12: IGAPI.search:output_type -> Reply
7, // [7:13] is the sub-list for method output_type
1, // [1:7] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_igapi_interface_proto_init() }
func file_igapi_interface_proto_init() {
if File_igapi_interface_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_igapi_interface_proto_rawDesc), len(file_igapi_interface_proto_rawDesc)),
NumEnums: 0,
NumMessages: 3,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_igapi_interface_proto_goTypes,
DependencyIndexes: file_igapi_interface_proto_depIdxs,
MessageInfos: file_igapi_interface_proto_msgTypes,
}.Build()
File_igapi_interface_proto = out.File
file_igapi_interface_proto_goTypes = nil
file_igapi_interface_proto_depIdxs = nil
}

26
igapi/interface.proto Normal file
View file

@ -0,0 +1,26 @@
syntax = "proto3";
option go_package = "nim.jasinco.work/app/igapi";
service IGAPI {
rpc login(Request) returns (Reply) {}
rpc account_info(Request) returns (Reply) {}
rpc upload(Request) returns (Reply) {}
rpc delete (Request) returns (Reply) {}
rpc queue(Request) returns (Reply) {}
rpc search(Request) returns (Reply) {}
}
message Request {
int64 id = 1;
string igid = 2;
}
message Reply {
int64 err = 1;
map<string, string> result = 2;
}

311
igapi/interface_grpc.pb.go Normal file
View file

@ -0,0 +1,311 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v6.31.0
// source: igapi/interface.proto
package igapi
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
IGAPI_Login_FullMethodName = "/IGAPI/login"
IGAPI_AccountInfo_FullMethodName = "/IGAPI/account_info"
IGAPI_Upload_FullMethodName = "/IGAPI/upload"
IGAPI_Delete_FullMethodName = "/IGAPI/delete"
IGAPI_Queue_FullMethodName = "/IGAPI/queue"
IGAPI_Search_FullMethodName = "/IGAPI/search"
)
// IGAPIClient is the client API for IGAPI service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type IGAPIClient interface {
Login(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
AccountInfo(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
Upload(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
Delete(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
Queue(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
Search(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error)
}
type iGAPIClient struct {
cc grpc.ClientConnInterface
}
func NewIGAPIClient(cc grpc.ClientConnInterface) IGAPIClient {
return &iGAPIClient{cc}
}
func (c *iGAPIClient) Login(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Reply)
err := c.cc.Invoke(ctx, IGAPI_Login_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *iGAPIClient) AccountInfo(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Reply)
err := c.cc.Invoke(ctx, IGAPI_AccountInfo_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *iGAPIClient) Upload(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Reply)
err := c.cc.Invoke(ctx, IGAPI_Upload_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *iGAPIClient) Delete(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Reply)
err := c.cc.Invoke(ctx, IGAPI_Delete_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *iGAPIClient) Queue(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Reply)
err := c.cc.Invoke(ctx, IGAPI_Queue_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *iGAPIClient) Search(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Reply, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(Reply)
err := c.cc.Invoke(ctx, IGAPI_Search_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// IGAPIServer is the server API for IGAPI service.
// All implementations must embed UnimplementedIGAPIServer
// for forward compatibility.
type IGAPIServer interface {
Login(context.Context, *Request) (*Reply, error)
AccountInfo(context.Context, *Request) (*Reply, error)
Upload(context.Context, *Request) (*Reply, error)
Delete(context.Context, *Request) (*Reply, error)
Queue(context.Context, *Request) (*Reply, error)
Search(context.Context, *Request) (*Reply, error)
mustEmbedUnimplementedIGAPIServer()
}
// UnimplementedIGAPIServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedIGAPIServer struct{}
func (UnimplementedIGAPIServer) Login(context.Context, *Request) (*Reply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
}
func (UnimplementedIGAPIServer) AccountInfo(context.Context, *Request) (*Reply, error) {
return nil, status.Errorf(codes.Unimplemented, "method AccountInfo not implemented")
}
func (UnimplementedIGAPIServer) Upload(context.Context, *Request) (*Reply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Upload not implemented")
}
func (UnimplementedIGAPIServer) Delete(context.Context, *Request) (*Reply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Delete not implemented")
}
func (UnimplementedIGAPIServer) Queue(context.Context, *Request) (*Reply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Queue not implemented")
}
func (UnimplementedIGAPIServer) Search(context.Context, *Request) (*Reply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Search not implemented")
}
func (UnimplementedIGAPIServer) mustEmbedUnimplementedIGAPIServer() {}
func (UnimplementedIGAPIServer) testEmbeddedByValue() {}
// UnsafeIGAPIServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to IGAPIServer will
// result in compilation errors.
type UnsafeIGAPIServer interface {
mustEmbedUnimplementedIGAPIServer()
}
func RegisterIGAPIServer(s grpc.ServiceRegistrar, srv IGAPIServer) {
// If the following call pancis, it indicates UnimplementedIGAPIServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&IGAPI_ServiceDesc, srv)
}
func _IGAPI_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IGAPIServer).Login(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IGAPI_Login_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IGAPIServer).Login(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
func _IGAPI_AccountInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IGAPIServer).AccountInfo(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IGAPI_AccountInfo_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IGAPIServer).AccountInfo(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
func _IGAPI_Upload_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IGAPIServer).Upload(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IGAPI_Upload_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IGAPIServer).Upload(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
func _IGAPI_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IGAPIServer).Delete(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IGAPI_Delete_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IGAPIServer).Delete(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
func _IGAPI_Queue_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IGAPIServer).Queue(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IGAPI_Queue_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IGAPIServer).Queue(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
func _IGAPI_Search_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(Request)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(IGAPIServer).Search(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: IGAPI_Search_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(IGAPIServer).Search(ctx, req.(*Request))
}
return interceptor(ctx, in, info, handler)
}
// IGAPI_ServiceDesc is the grpc.ServiceDesc for IGAPI service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var IGAPI_ServiceDesc = grpc.ServiceDesc{
ServiceName: "IGAPI",
HandlerType: (*IGAPIServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "login",
Handler: _IGAPI_Login_Handler,
},
{
MethodName: "account_info",
Handler: _IGAPI_AccountInfo_Handler,
},
{
MethodName: "upload",
Handler: _IGAPI_Upload_Handler,
},
{
MethodName: "delete",
Handler: _IGAPI_Delete_Handler,
},
{
MethodName: "queue",
Handler: _IGAPI_Queue_Handler,
},
{
MethodName: "search",
Handler: _IGAPI_Search_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "igapi/interface.proto",
}

270
internal/handlers/admin.go Normal file
View file

@ -0,0 +1,270 @@
package handlers
import (
"bytes"
"encoding/base64"
"image/png"
"log"
"path"
"time"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/pquerna/otp/totp"
"golang.org/x/crypto/scrypt"
"nim.jasinco.work/app/internal"
"nim.jasinco.work/app/nimdb"
)
type NewAdmin struct {
Name string `json:"name"`
Password string `json:"password"`
TOTP_Secret string `json:"totp_secret"`
}
type AdminAuth struct {
Name string `json:"name"`
Password string `json:"password"`
TOTP_code string `json:"totp_code"`
}
func Admin_create(c *fiber.Ctx) error {
cfg := new(NewAdmin)
if err := c.BodyParser(cfg); err != nil {
return fiber.ErrBadRequest
}
hashed, err := scrypt.Key([]byte(cfg.Password), []byte(internal.SALT), 32768, 8, 1, 32)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
err = internal.NIMDB.AdminCreateAccount(c.Context(), nimdb.AdminCreateAccountParams{Username: cfg.Name, Password: base64.StdEncoding.EncodeToString(hashed), Totp: cfg.TOTP_Secret})
if err != nil {
log.Println(base64.StdEncoding.EncodeToString(hashed), err)
return c.SendStatus(fiber.StatusBadRequest)
}
return c.SendStatus(fiber.StatusCreated)
}
func Admin_new_totp_code(c *fiber.Ctx) error {
name := c.Query("name")
if len(name) == 0 {
return fiber.ErrBadRequest
}
key, err := totp.Generate(totp.GenerateOpts{Issuer: "TCIVS_NIMING", AccountName: name})
if err != nil {
return fiber.ErrInternalServerError
}
var buf bytes.Buffer
img, err := key.Image(200, 200)
png.Encode(&buf, img)
return c.JSON(fiber.Map{"key": key.Secret(), "img": base64.StdEncoding.EncodeToString(buf.Bytes())})
}
const debug = false
func Admin_Login_JWT(c *fiber.Ctx) (*string, error) {
cred := new(AdminAuth)
if err := c.BodyParser(cred); err != nil {
return nil, fiber.ErrBadRequest
}
hashed, err := scrypt.Key([]byte(cred.Password), []byte(internal.SALT), 32768, 8, 1, 32)
if err != nil {
return nil, fiber.ErrInternalServerError
}
var claims jwt.MapClaims
if !debug {
rec, err := internal.NIMDB.AdminLoginGetTOTP(c.Context(), nimdb.AdminLoginGetTOTPParams{Username: cred.Name, Password: base64.StdEncoding.EncodeToString(hashed)})
if err != nil {
log.Println(err)
c.SendString("Failed to login with wrong account or password")
return nil, fiber.ErrUnauthorized
}
if !totp.Validate(cred.TOTP_code, rec.Totp) {
log.Println(totp.GenerateCode(rec.Totp, time.Now()))
c.SendString("Failed to login with wrong totp")
return nil, fiber.ErrUnauthorized
}
claims = jwt.MapClaims{"name": cred.Name, "admin": rec.Super}
} else {
claims = jwt.MapClaims{"name": "test", "admin": true}
}
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
t, err := token.SignedString([]byte(internal.JWT_SECRET))
if err != nil {
return nil, fiber.ErrInternalServerError
}
cookie := new(fiber.Cookie)
cookie.Name = "token"
cookie.Value = t
cookie.Expires = time.Now().Add(5 * time.Hour)
c.Cookie(cookie)
return &t, nil
}
func Admin_Fetch_Post(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
super := claims["admin"].(bool)
ctx := c.Context()
if !super {
rec, err := internal.NIMDB.AdminGetPost(ctx)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
type recwithimg struct {
Id int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
Post_at time.Time `json:"post_at"`
Enclosue []string `json:"enclosure"`
}
rec_with_img := make([]recwithimg, len(rec))
for id, k := range rec {
rec_with_img[id] = recwithimg{Id: k.ID, Content: k.Content, Signing: k.Signing, Post_at: k.PostAt.Time}
imgrec, err := internal.NIMDB.AdminGetMedia(ctx, pgtype.Int4{Int32: k.ID, Valid: true})
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
rec_with_img[id].Enclosue = imgrec
}
return c.JSON(rec_with_img)
} else {
rec, err := internal.NIMDB.SuperAdminGetPost(ctx)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
type recwithimg struct {
Id int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
Post_at time.Time `json:"post_at"`
Enclosue []string `json:"enclosure"`
Phase nimdb.PostPhase `json:"phase"`
}
rec_with_img := make([]recwithimg, len(rec))
for id, k := range rec {
rec_with_img[id] = recwithimg{Id: k.ID, Content: k.Content, Signing: k.Signing, Post_at: k.PostAt.Time, Phase: k.Phase}
imgrec, err := internal.NIMDB.AdminGetMedia(ctx, pgtype.Int4{Int32: k.ID, Valid: true})
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
rec_with_img[id].Enclosue = imgrec
}
return c.JSON(rec_with_img)
}
}
type Verify struct {
Id int32 `json:"post"`
Check bool `json:"check"`
}
type VerifyList struct {
Choice []Verify `json:"choice"`
}
func AdminVerify(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
super := claims["admin"].(bool)
ctx := c.Context()
tx, err := internal.POOL.Begin(ctx)
defer tx.Rollback(ctx)
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
qtx := internal.NIMDB.WithTx(tx)
post_verify_list := new(VerifyList)
if err = c.BodyParser(post_verify_list); err != nil {
log.Printf("Can't parse verify body: %s", err.Error())
return c.SendStatus(fiber.StatusBadRequest)
}
if !super {
for _, post_verify := range post_verify_list.Choice {
if post_verify.Check {
_, err = qtx.AdminVerify(ctx, nimdb.AdminVerifyParams{ID: post_verify.Id, Phase: nimdb.PostPhaseOk})
if err != nil {
log.Println(err.Error(), post_verify.Id)
return c.SendStatus(fiber.StatusBadRequest)
}
err = qtx.AdminUpdateImage(ctx, pgtype.Int4{Int32: post_verify.Id, Valid: true})
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
if internal.IGAPI_ACTIVATE {
internal.IGAPI_CHAN <- internal.PR{ACT_TYPE: 0, ID: post_verify.Id}
}
} else {
_, err = qtx.AdminVerify(ctx, nimdb.AdminVerifyParams{ID: post_verify.Id, Phase: nimdb.PostPhaseRejected})
if err != nil {
log.Println(err.Error())
return c.SendStatus(fiber.StatusBadRequest)
}
}
}
} else {
for _, post_verify := range post_verify_list.Choice {
if post_verify.Check {
_, err = qtx.SuperAdminVerify(ctx, nimdb.SuperAdminVerifyParams{ID: post_verify.Id, Phase: nimdb.PostPhaseOk})
if err != nil {
log.Println(err.Error(), post_verify.Id)
return c.SendStatus(fiber.StatusBadRequest)
}
err = qtx.AdminUpdateImage(ctx, pgtype.Int4{Int32: post_verify.Id, Valid: true})
if err != nil {
return c.SendStatus(fiber.StatusInternalServerError)
}
if internal.IGAPI_ACTIVATE {
internal.IGAPI_CHAN <- internal.PR{ACT_TYPE: 0, ID: post_verify.Id}
}
} else {
_, err = qtx.SuperAdminVerify(ctx, nimdb.SuperAdminVerifyParams{ID: post_verify.Id, Phase: nimdb.PostPhaseAdminRejected})
if err != nil {
log.Println(err.Error())
return c.SendStatus(fiber.StatusBadRequest)
}
}
}
}
return tx.Commit(ctx)
}
const adminpage_basepath = "./admin_panel/dist"
func AdminSendPage(c *fiber.Ctx) error {
pagepath := c.Params("*")
if pagepath == "login" || pagepath == "panel" || pagepath == "new_account" {
err := c.SendFile(path.Join(adminpage_basepath, "index.html"))
if err != nil {
log.Println(err)
return c.SendStatus(fiber.StatusNotFound)
}
return nil
}
err := c.SendFile(path.Join(adminpage_basepath, pagepath))
if err != nil {
c.SendStatus(404)
}
return nil
}

View file

@ -0,0 +1,17 @@
package handlers
import (
"strconv"
"github.com/gofiber/fiber/v2"
"nim.jasinco.work/app/internal"
)
func Add_heart(c *fiber.Ctx) error {
hearts, err := strconv.Atoi(c.Query("post"))
if err != nil {
return fiber.ErrBadRequest
}
dbhearts, err := internal.NIMDB.AddPostHeart(c.Context(), int32(hearts))
return c.JSON(fiber.Map{"hearts": dbhearts})
}

182
internal/handlers/post.go Normal file
View file

@ -0,0 +1,182 @@
package handlers
import (
"crypto/sha256"
"encoding/base64"
"log"
"mime"
"path"
"strconv"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
"nim.jasinco.work/app/internal"
"nim.jasinco.work/app/nimdb"
)
var supported_filetype = []string{"image/png", "image/jpeg", "image/webp", "image/gif", "image/heif", "video/H264", "video/H265", "video/mp4"}
func find_supported(mimetype string) bool {
for _, supported := range supported_filetype {
if strings.Compare(supported, mimetype) == 0 {
return true
}
}
return false
}
func Fetch_post(c *fiber.Ctx) error {
cursor := c.Query("cursor")
ctx := c.Context()
var rec []nimdb.GetPostRow
var err error
if len(cursor) == 0 {
rec, err = internal.NIMDB.GetPost(ctx, internal.FETCH_LENGTH)
if err != nil {
log.Println(err)
return fiber.ErrInternalServerError
}
} else if cursor_num, err := strconv.Atoi(cursor); err == nil {
rec_cur, err := internal.NIMDB.GetPostWithCursor(ctx, nimdb.GetPostWithCursorParams{ID: int32(cursor_num), Limit: internal.FETCH_LENGTH})
if err != nil {
log.Println(err)
return c.Status(500).SendString("Failed to Query DB")
}
rec = make([]nimdb.GetPostRow, len(rec_cur))
for id, val := range rec_cur {
rec[id] = nimdb.GetPostRow(val)
}
} else {
return c.Status(fiber.StatusBadRequest).SendString("Can't parse cursor")
}
if len(rec) == 0 {
return c.SendStatus(fiber.StatusNoContent)
}
return c.JSON(rec)
}
func Delete_post(c *fiber.Ctx) error {
post := c.Query("post")
hash := c.Query("hash")
ctx := c.Context()
post_id, err := strconv.Atoi(post)
post_id_i32 := int32(post_id)
if err != nil {
return c.Status(500).SendString("Failed to Parse Int")
}
tx, err := internal.POOL.Begin(ctx)
if err != nil {
return c.Status(500).SendString("Failed to Start Transaction")
}
defer tx.Rollback(ctx)
qtx := internal.NIMDB.WithTx(tx)
if err = qtx.DeletePost(ctx, nimdb.DeletePostParams{ID: post_id_i32, Hash: hash}); err != nil {
return err
}
if err = qtx.InvalidateMedia(ctx, pgtype.Int4{Int32: post_id_i32}); err != nil {
return err
}
if tx.Commit(ctx) != nil {
return c.Status(500).SendString("Failed to Commit")
}
if internal.IGAPI_ACTIVATE {
internal.IGAPI_CHAN <- internal.PR{ACT_TYPE: 1, ID: post_id_i32}
}
return c.SendStatus(200)
}
func Insert_Post(c *fiber.Ctx) error {
ctx := c.Context()
tx, err := internal.POOL.Begin(ctx)
if err != nil {
return c.Status(500).SendString("Failed to Start Transaction")
}
defer tx.Rollback(ctx)
qtx := internal.NIMDB.WithTx(tx)
if form, err := c.MultipartForm(); err == nil {
content := form.Value["content"]
signing := form.Value["signing"]
files := form.File["enclosure"]
if len(content) == 0 || len(content[0]) == 0 {
return fiber.ErrBadRequest
}
post_hash := sha256.New()
post_hash.Write([]byte(content[0]))
signing_pgt := new(pgtype.Text)
signing_pgt.Valid = false
if len(signing) > 0 && len(signing[0]) > 0 {
post_hash.Write([]byte(signing[0]))
signing_pgt.Valid = true
signing_pgt.String = signing[0]
}
now, err := time.Now().MarshalBinary()
if err != nil {
c.SendString("Failed to get time")
return c.SendStatus(500)
}
credential, err := qtx.InsertPost(ctx,
nimdb.InsertPostParams{
Content: content[0],
Signing: *signing_pgt,
Hash: base64.StdEncoding.EncodeToString(post_hash.Sum(now)),
},
)
if err != nil {
log.Println(err.Error())
return c.Status(500).SendString("Failed to insert post")
}
for _, file := range files {
filemime, _, err := mime.ParseMediaType(file.Header.Get("Content-Type"))
if err != nil {
return c.Status(fiber.ErrBadRequest.Code).SendString("Failed to parse content type")
}
if !find_supported(filemime) {
return c.Status(fiber.ErrBadRequest.Code).SendString("Unsupported Type")
}
filetype := strings.Split(filemime, "/")[0]
mid, err := uuid.NewRandom()
if err != nil {
c.SendString("Faild to generate UUIDv4")
return c.SendStatus(500)
}
fxnamepath := filetype + "/" + mid.String()
if err = c.SaveFile(file, path.Join("./static", fxnamepath)); err != nil {
log.Println("Can't save to ./static", err.Error())
return fiber.ErrInternalServerError
}
if err := qtx.InsertPostImage(ctx, nimdb.InsertPostImageParams{
Url: fxnamepath,
PostID: pgtype.Int4{Int32: credential.ID, Valid: true},
}); err != nil {
log.Println(err.Error())
return c.Status(500).SendString("Failed to insert image")
}
}
if err = tx.Commit(ctx); err != nil {
log.Println(err.Error())
c.SendString("Failed to send Commit")
return c.SendStatus(500)
}
return c.JSON(credential)
}
return fiber.ErrBadRequest
}

9
internal/pool.go Normal file
View file

@ -0,0 +1,9 @@
package internal
import (
"github.com/jackc/pgx/v5/pgxpool"
"nim.jasinco.work/app/nimdb"
)
var POOL *pgxpool.Pool = nil
var NIMDB *nimdb.Queries = nil

97
internal/settings.go Normal file
View file

@ -0,0 +1,97 @@
package internal
import (
"context"
"errors"
"log"
"os"
"strconv"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"nim.jasinco.work/app/igapi"
)
type PR struct {
ACT_TYPE int //0 for post, 1 for revoke
ID int32
}
var (
SALT string
POSTGRES_URL string
JWT_SECRET string
FETCH_LENGTH int32
PREFORK bool
IGAPI_HOST string
CORS_ALLOW string
IGAPI_ACTIVATE bool
IGAPI_CHAN = make(chan PR, 20)
err error
conv int64
)
func ReadFromENV() error {
SALT = os.Getenv("SALT")
if len(SALT) < 8 {
return errors.New("Invalid Salt")
}
POSTGRES_URL = os.Getenv("POSTGRES_URL")
if len(POSTGRES_URL) < 1 {
return errors.New("POSTGRES_URL NOT FOUND")
}
JWT_SECRET = os.Getenv("JWT_SECRET")
if len(JWT_SECRET) < 10 {
return errors.New("INVALID JWT SECRET")
}
fetch_len_str := os.Getenv("FETCH_LENGTH")
if len(fetch_len_str) == 0 {
FETCH_LENGTH = 10
} else {
conv, err = strconv.ParseInt(fetch_len_str, 10, 32)
if err != nil {
return err
}
if conv < 1 {
return errors.New("FETCH_LENGTH should be a positive number as well over 0")
}
FETCH_LENGTH = int32(conv)
}
prefork_str := os.Getenv("PREFORK")
if len(prefork_str) == 0 {
PREFORK = false
} else {
PREFORK, err = strconv.ParseBool(prefork_str)
if err != nil {
return err
}
}
IGAPI_HOST = os.Getenv("IGAPI_HOST")
IGAPI_ACTIVATE = true
if len(IGAPI_HOST) == 0 {
log.Println("Didn't get IGAPI_HOST, it will work without it")
IGAPI_ACTIVATE = false
}
CORS_ALLOW = os.Getenv("CORS_ALLOW")
return nil
}
func IGAPI_establish() (*grpc.ClientConn, igapi.IGAPIClient) {
conn, err := grpc.NewClient(IGAPI_HOST, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Panicf("Can't connect to igapi: %s", err.Error())
}
c := igapi.NewIGAPIClient(conn)
return conn, c
}
func IGAPI_chan_exec(c igapi.IGAPIClient) {
for id := range IGAPI_CHAN {
if id.ACT_TYPE == 0 {
c.Upload(context.Background(), &igapi.Request{Id: int64(id.ID)})
}
if id.ACT_TYPE == 1 {
c.Delete(context.Background(), &igapi.Request{Id: int64(id.ID)})
}
}
}

View file

@ -1,6 +0,0 @@
fake:
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/gen_fake.ts
fetch_post:
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/post_db.ts
create_schema:
POSTGRES_URL="postgres://test:test@192.168.50.14:5432/posts" bun run ./tools/create_schema.ts

View file

@ -1,7 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;

32
nimdb/db.go Normal file
View file

@ -0,0 +1,32 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package nimdb
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,
}
}

92
nimdb/models.go Normal file
View file

@ -0,0 +1,92 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
package nimdb
import (
"database/sql/driver"
"fmt"
"github.com/jackc/pgx/v5/pgtype"
)
type PostPhase string
const (
PostPhasePending PostPhase = "pending"
PostPhaseRejected PostPhase = "rejected"
PostPhaseAdminRejected PostPhase = "admin_rejected"
PostPhaseOk PostPhase = "ok"
PostPhaseDeleted PostPhase = "deleted"
)
func (e *PostPhase) Scan(src interface{}) error {
switch s := src.(type) {
case []byte:
*e = PostPhase(s)
case string:
*e = PostPhase(s)
default:
return fmt.Errorf("unsupported scan type for PostPhase: %T", src)
}
return nil
}
type NullPostPhase struct {
PostPhase PostPhase `json:"post_phase"`
Valid bool `json:"valid"` // Valid is true if PostPhase is not NULL
}
// Scan implements the Scanner interface.
func (ns *NullPostPhase) Scan(value interface{}) error {
if value == nil {
ns.PostPhase, ns.Valid = "", false
return nil
}
ns.Valid = true
return ns.PostPhase.Scan(value)
}
// Value implements the driver Valuer interface.
func (ns NullPostPhase) Value() (driver.Value, error) {
if !ns.Valid {
return nil, nil
}
return string(ns.PostPhase), nil
}
type Admin struct {
Username string `json:"username"`
Password string `json:"password"`
Totp string `json:"totp"`
Super bool `json:"super"`
}
type Comment struct {
ID int32 `json:"id"`
PostID int32 `json:"post_id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
Hash string `json:"hash"`
PostAt pgtype.Timestamptz `json:"post_at"`
Heart int32 `json:"heart"`
}
type Medium struct {
Url string `json:"url"`
PostID pgtype.Int4 `json:"post_id"`
CommentID pgtype.Int4 `json:"comment_id"`
Visible pgtype.Bool `json:"visible"`
}
type Post struct {
ID int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
Hash string `json:"hash"`
PostAt pgtype.Timestamptz `json:"post_at"`
Heart int32 `json:"heart"`
Phase PostPhase `json:"phase"`
Igid pgtype.Text `json:"igid"`
}

490
nimdb/query.sql.go Normal file
View file

@ -0,0 +1,490 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: query.sql
package nimdb
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const addCommentHeart = `-- name: AddCommentHeart :one
UPDATE comment SET heart = heart + 1 WHERE id=$1 RETURNING heart
`
func (q *Queries) AddCommentHeart(ctx context.Context, id int32) (int32, error) {
row := q.db.QueryRow(ctx, addCommentHeart, id)
var heart int32
err := row.Scan(&heart)
return heart, err
}
const addPostHeart = `-- name: AddPostHeart :one
UPDATE posts SET heart = heart + 1 WHERE id=$1 RETURNING heart
`
func (q *Queries) AddPostHeart(ctx context.Context, id int32) (int32, error) {
row := q.db.QueryRow(ctx, addPostHeart, id)
var heart int32
err := row.Scan(&heart)
return heart, err
}
const adminCreateAccount = `-- name: AdminCreateAccount :exec
INSERT INTO admin (username, password, totp) VALUES ($1, $2,$3)
`
type AdminCreateAccountParams struct {
Username string `json:"username"`
Password string `json:"password"`
Totp string `json:"totp"`
}
func (q *Queries) AdminCreateAccount(ctx context.Context, arg AdminCreateAccountParams) error {
_, err := q.db.Exec(ctx, adminCreateAccount, arg.Username, arg.Password, arg.Totp)
return err
}
const adminGetMedia = `-- name: AdminGetMedia :many
SELECT url from media WHERE post_id = $1
`
func (q *Queries) AdminGetMedia(ctx context.Context, postID pgtype.Int4) ([]string, error) {
rows, err := q.db.Query(ctx, adminGetMedia, postID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []string
for rows.Next() {
var url string
if err := rows.Scan(&url); err != nil {
return nil, err
}
items = append(items, url)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const adminGetPost = `-- name: AdminGetPost :many
SELECT id, content, signing, post_at FROM posts WHERE phase = 'pending' ORDER BY id DESC LIMIT 100
`
type AdminGetPostRow struct {
ID int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
PostAt pgtype.Timestamptz `json:"post_at"`
}
func (q *Queries) AdminGetPost(ctx context.Context) ([]AdminGetPostRow, error) {
rows, err := q.db.Query(ctx, adminGetPost)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AdminGetPostRow
for rows.Next() {
var i AdminGetPostRow
if err := rows.Scan(
&i.ID,
&i.Content,
&i.Signing,
&i.PostAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const adminLoginGetTOTP = `-- name: AdminLoginGetTOTP :one
SELECT totp, super FROM admin WHERE username = $1 AND password = $2
`
type AdminLoginGetTOTPParams struct {
Username string `json:"username"`
Password string `json:"password"`
}
type AdminLoginGetTOTPRow struct {
Totp string `json:"totp"`
Super bool `json:"super"`
}
func (q *Queries) AdminLoginGetTOTP(ctx context.Context, arg AdminLoginGetTOTPParams) (AdminLoginGetTOTPRow, error) {
row := q.db.QueryRow(ctx, adminLoginGetTOTP, arg.Username, arg.Password)
var i AdminLoginGetTOTPRow
err := row.Scan(&i.Totp, &i.Super)
return i, err
}
const adminUpdateImage = `-- name: AdminUpdateImage :exec
UPDATE media SET visible = true WHERE post_id=$1
`
func (q *Queries) AdminUpdateImage(ctx context.Context, postID pgtype.Int4) error {
_, err := q.db.Exec(ctx, adminUpdateImage, postID)
return err
}
const adminVerify = `-- name: AdminVerify :one
UPDATE posts SET phase = $1 WHERE id=$2 AND phase = 'pending' RETURNING id
`
type AdminVerifyParams struct {
Phase PostPhase `json:"phase"`
ID int32 `json:"id"`
}
func (q *Queries) AdminVerify(ctx context.Context, arg AdminVerifyParams) (int32, error) {
row := q.db.QueryRow(ctx, adminVerify, arg.Phase, arg.ID)
var id int32
err := row.Scan(&id)
return id, err
}
const deletePost = `-- name: DeletePost :exec
UPDATE posts SET phase = 'deleted' WHERE id = $1 AND hash=$2
`
type DeletePostParams struct {
ID int32 `json:"id"`
Hash string `json:"hash"`
}
func (q *Queries) DeletePost(ctx context.Context, arg DeletePostParams) error {
_, err := q.db.Exec(ctx, deletePost, arg.ID, arg.Hash)
return err
}
const getComment = `-- name: GetComment :many
SELECT id, content, signing, post_at
FROM comment
WHERE phase = 'ok' AND post_id = $1
ORDER BY id DESC
LIMIT $2
`
type GetCommentParams struct {
PostID int32 `json:"post_id"`
Limit int32 `json:"limit"`
}
type GetCommentRow struct {
ID int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
PostAt pgtype.Timestamptz `json:"post_at"`
}
func (q *Queries) GetComment(ctx context.Context, arg GetCommentParams) ([]GetCommentRow, error) {
rows, err := q.db.Query(ctx, getComment, arg.PostID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCommentRow
for rows.Next() {
var i GetCommentRow
if err := rows.Scan(
&i.ID,
&i.Content,
&i.Signing,
&i.PostAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getCommentWithCursor = `-- name: GetCommentWithCursor :many
SELECT id, content, signing, post_at
FROM comment
WHERE id <= $1 AND phase = 'ok' AND post_id = $2
ORDER BY id DESC
LIMIT $3
`
type GetCommentWithCursorParams struct {
ID int32 `json:"id"`
PostID int32 `json:"post_id"`
Limit int32 `json:"limit"`
}
type GetCommentWithCursorRow struct {
ID int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
PostAt pgtype.Timestamptz `json:"post_at"`
}
func (q *Queries) GetCommentWithCursor(ctx context.Context, arg GetCommentWithCursorParams) ([]GetCommentWithCursorRow, error) {
rows, err := q.db.Query(ctx, getCommentWithCursor, arg.ID, arg.PostID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCommentWithCursorRow
for rows.Next() {
var i GetCommentWithCursorRow
if err := rows.Scan(
&i.ID,
&i.Content,
&i.Signing,
&i.PostAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPost = `-- name: GetPost :many
SELECT id, content, signing, post_at, heart, igid, array_agg(url) as enclosure
FROM posts
LEFT JOIN media ON post_id is not null AND post_id = id AND visible
WHERE phase = 'ok'
GROUP BY id
ORDER BY id DESC
LIMIT $1
`
type GetPostRow struct {
ID int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
PostAt pgtype.Timestamptz `json:"post_at"`
Heart int32 `json:"heart"`
Igid pgtype.Text `json:"igid"`
Enclosure interface{} `json:"enclosure"`
}
func (q *Queries) GetPost(ctx context.Context, limit int32) ([]GetPostRow, error) {
rows, err := q.db.Query(ctx, getPost, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPostRow
for rows.Next() {
var i GetPostRow
if err := rows.Scan(
&i.ID,
&i.Content,
&i.Signing,
&i.PostAt,
&i.Heart,
&i.Igid,
&i.Enclosure,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getPostWithCursor = `-- name: GetPostWithCursor :many
SELECT id, content, signing, post_at, heart, igid, array_agg(url) as enclosure
FROM posts
LEFT JOIN media ON post_id is not null AND post_id = id AND visible
WHERE id <= $1 AND phase = 'ok'
GROUP BY id
ORDER BY id DESC
LIMIT $2
`
type GetPostWithCursorParams struct {
ID int32 `json:"id"`
Limit int32 `json:"limit"`
}
type GetPostWithCursorRow struct {
ID int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
PostAt pgtype.Timestamptz `json:"post_at"`
Heart int32 `json:"heart"`
Igid pgtype.Text `json:"igid"`
Enclosure interface{} `json:"enclosure"`
}
func (q *Queries) GetPostWithCursor(ctx context.Context, arg GetPostWithCursorParams) ([]GetPostWithCursorRow, error) {
rows, err := q.db.Query(ctx, getPostWithCursor, arg.ID, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPostWithCursorRow
for rows.Next() {
var i GetPostWithCursorRow
if err := rows.Scan(
&i.ID,
&i.Content,
&i.Signing,
&i.PostAt,
&i.Heart,
&i.Igid,
&i.Enclosure,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const insertCommentImage = `-- name: InsertCommentImage :exec
INSERT INTO media (url, comment_id) VALUES ($1, $2)
`
type InsertCommentImageParams struct {
Url string `json:"url"`
CommentID pgtype.Int4 `json:"comment_id"`
}
func (q *Queries) InsertCommentImage(ctx context.Context, arg InsertCommentImageParams) error {
_, err := q.db.Exec(ctx, insertCommentImage, arg.Url, arg.CommentID)
return err
}
const insertPost = `-- name: InsertPost :one
INSERT INTO posts (content,signing,hash)
VALUES ($1, $2 ,$3)
RETURNING id,hash
`
type InsertPostParams struct {
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
Hash string `json:"hash"`
}
type InsertPostRow struct {
ID int32 `json:"id"`
Hash string `json:"hash"`
}
func (q *Queries) InsertPost(ctx context.Context, arg InsertPostParams) (InsertPostRow, error) {
row := q.db.QueryRow(ctx, insertPost, arg.Content, arg.Signing, arg.Hash)
var i InsertPostRow
err := row.Scan(&i.ID, &i.Hash)
return i, err
}
const insertPostImage = `-- name: InsertPostImage :exec
INSERT INTO media (url, post_id) VALUES ($1, $2)
`
type InsertPostImageParams struct {
Url string `json:"url"`
PostID pgtype.Int4 `json:"post_id"`
}
func (q *Queries) InsertPostImage(ctx context.Context, arg InsertPostImageParams) error {
_, err := q.db.Exec(ctx, insertPostImage, arg.Url, arg.PostID)
return err
}
const invalidateMedia = `-- name: InvalidateMedia :exec
UPDATE media SET visible = false WHERE post_id = $1
`
func (q *Queries) InvalidateMedia(ctx context.Context, postID pgtype.Int4) error {
_, err := q.db.Exec(ctx, invalidateMedia, postID)
return err
}
const superAdminGetPost = `-- name: SuperAdminGetPost :many
SELECT id, content, signing, post_at, phase FROM posts WHERE phase = 'pending' OR phase = 'rejected' ORDER BY id DESC LIMIT 100
`
type SuperAdminGetPostRow struct {
ID int32 `json:"id"`
Content string `json:"content"`
Signing pgtype.Text `json:"signing"`
PostAt pgtype.Timestamptz `json:"post_at"`
Phase PostPhase `json:"phase"`
}
func (q *Queries) SuperAdminGetPost(ctx context.Context) ([]SuperAdminGetPostRow, error) {
rows, err := q.db.Query(ctx, superAdminGetPost)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SuperAdminGetPostRow
for rows.Next() {
var i SuperAdminGetPostRow
if err := rows.Scan(
&i.ID,
&i.Content,
&i.Signing,
&i.PostAt,
&i.Phase,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const superAdminVerify = `-- name: SuperAdminVerify :one
UPDATE posts SET phase = $1 WHERE id=$2 AND (phase = 'pending' OR phase = 'rejected') RETURNING id
`
type SuperAdminVerifyParams struct {
Phase PostPhase `json:"phase"`
ID int32 `json:"id"`
}
func (q *Queries) SuperAdminVerify(ctx context.Context, arg SuperAdminVerifyParams) (int32, error) {
row := q.db.QueryRow(ctx, superAdminVerify, arg.Phase, arg.ID)
var id int32
err := row.Scan(&id)
return id, err
}
const updatePostIGID = `-- name: UpdatePostIGID :exec
UPDATE posts set igid = $1 WHERE id = $2
`
type UpdatePostIGIDParams struct {
Igid pgtype.Text `json:"igid"`
ID int32 `json:"id"`
}
func (q *Queries) UpdatePostIGID(ctx context.Context, arg UpdatePostIGIDParams) error {
_, err := q.db.Exec(ctx, updatePostIGID, arg.Igid, arg.ID)
return err
}

View file

@ -1,26 +0,0 @@
{
"name": "nim",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@faker-js/faker": "^9.7.0",
"bun-types": "^1.2.9",
"next": "15.3.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"typescript": "^5",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@tailwindcss/postcss": "^4",
"tailwindcss": "^4"
}
}

View file

@ -1,5 +0,0 @@
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;

View file

@ -1 +0,0 @@
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

Before

Width:  |  Height:  |  Size: 391 B

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 128 B

View file

@ -1 +0,0 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

Before

Width:  |  Height:  |  Size: 385 B

129
server.go Normal file
View file

@ -0,0 +1,129 @@
package main
import (
"context"
"log"
"regexp"
"strings"
"time"
jwtware "github.com/gofiber/contrib/jwt"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/gofiber/fiber/v2/middleware/limiter"
"github.com/golang-jwt/jwt/v5"
"github.com/jackc/pgx/v5/pgxpool"
"nim.jasinco.work/app/internal"
"nim.jasinco.work/app/internal/handlers"
"nim.jasinco.work/app/nimdb"
)
func main() {
err := internal.ReadFromENV()
if err != nil {
log.Fatal(err.Error())
}
dbpool, err := pgxpool.New(context.Background(), internal.POSTGRES_URL)
if err != nil {
log.Fatal("Failed to open connection to db")
}
defer dbpool.Close()
internal.POOL = dbpool
internal.NIMDB = nimdb.New(dbpool)
if internal.IGAPI_ACTIVATE {
conn, client := internal.IGAPI_establish()
defer conn.Close()
go internal.IGAPI_chan_exec(client)
}
app := fiber.New(fiber.Config{Prefork: internal.PREFORK})
app.Static("/", "./static/webpage/")
app.Use(limiter.New(limiter.Config{
Next: func(c *fiber.Ctx) bool {
return c.IP() == "127.0.0.1"
},
Max: 50,
Expiration: 30 * time.Second,
KeyGenerator: func(c *fiber.Ctx) string {
return c.Get("x-forwarded-for")
},
LimitReached: func(c *fiber.Ctx) error {
return c.SendFile("./toofast.html")
},
}))
if len(internal.CORS_ALLOW) > 0 {
app.Use(cors.New(cors.Config{
AllowOrigins: internal.CORS_ALLOW,
AllowCredentials: true,
}))
}
app.Get("/api/post", handlers.Fetch_post)
app.Post("/api/post", handlers.Insert_Post)
app.Delete("/api/post", handlers.Delete_post)
app.Get("/api/heart", handlers.Add_heart)
app.Static("/static", "./static/")
app.Get("/admin", func(c *fiber.Ctx) error {
return c.Redirect("/admin/login")
})
app.Post("/api/admin/login", func(c *fiber.Ctx) error {
token, err := handlers.Admin_Login_JWT(c)
if err != nil {
return err
}
return c.JSON(fiber.Map{"token": *token})
})
app.Post("/admin/login", func(c *fiber.Ctx) error {
_, err := handlers.Admin_Login_JWT(c)
if err != nil {
return err
}
return c.Redirect("/admin/panel")
})
app.Static("/admin/login", "./admin/login", fiber.Static{Index: "index.html"})
app.Use(jwtware.New(jwtware.Config{
SigningKey: jwtware.SigningKey{Key: []byte(internal.JWT_SECRET)},
TokenLookup: "cookie:token",
ErrorHandler: func(ctx *fiber.Ctx, err error) error {
webpanel, regxerr := regexp.Match("^/admin/.*", []byte(ctx.Path()))
if regxerr == nil && webpanel {
return ctx.Redirect("/admin/login")
} else if regxerr != nil {
log.Printf("RegErr %s\n", regxerr.Error())
}
if !strings.ContainsAny(ctx.Path(), "admin") {
return ctx.SendStatus(404)
}
if err == jwtware.ErrJWTMissingOrMalformed {
return ctx.Status(400).SendString(err.Error())
}
if err == jwtware.ErrJWTAlg {
return ctx.Status(401).SendString(err.Error())
}
return nil
},
}))
app.Static("/admin/panel", "./admin/panel", fiber.Static{Index: "index.html"})
app.Use("/admin/create", func(c *fiber.Ctx) error {
user := c.Locals("user").(*jwt.Token)
claims := user.Claims.(jwt.MapClaims)
super := claims["admin"].(bool)
if !super {
return c.SendStatus(fiber.StatusNotFound)
}
return c.Next()
})
app.Static("/admin/create", "./admin/create", fiber.Static{Index: "index.html"})
app.Get("/api/admin/new_totp", handlers.Admin_new_totp_code)
app.Get("/api/admin/fetch_post", handlers.Admin_Fetch_Post)
app.Put("/api/admin/verify_post", handlers.AdminVerify)
app.Post("/api/admin/create", handlers.Admin_create)
log.Panic(app.Listen(":3000"))
}

View file

@ -1,15 +0,0 @@
services:
postgres:
image: postgres:17.4-alpine3.21
restart: always
shm_size: 128mb
environment:
- POSTGRES_PASSWORD=test
- POSTGRES_USER=test
ports:
- 5432:5432
adminer:
image: adminer
restart: always
ports:
- 8080:8080

82
sql/query.sql Normal file
View file

@ -0,0 +1,82 @@
-- name: GetPost :many
SELECT id, content, signing, post_at, heart, igid, array_agg(url) as enclosure
FROM posts
LEFT JOIN media ON post_id is not null AND post_id = id AND visible
WHERE phase = 'ok'
GROUP BY id
ORDER BY id DESC
LIMIT $1;
-- name: GetPostWithCursor :many
SELECT id, content, signing, post_at, heart, igid, array_agg(url) as enclosure
FROM posts
LEFT JOIN media ON post_id is not null AND post_id = id AND visible
WHERE id <= $1 AND phase = 'ok'
GROUP BY id
ORDER BY id DESC
LIMIT $2;
-- name: InsertPost :one
INSERT INTO posts (content,signing,hash)
VALUES ($1, $2 ,$3)
RETURNING id,hash;
-- name: UpdatePostIGID :exec
UPDATE posts set igid = $1 WHERE id = $2;
-- name: DeletePost :exec
UPDATE posts SET phase = 'deleted' WHERE id = $1 AND hash=$2;
-- name: InvalidateMedia :exec
UPDATE media SET visible = false WHERE post_id = $1;
-- name: GetComment :many
SELECT id, content, signing, post_at
FROM comment
WHERE phase = 'ok' AND post_id = $1
ORDER BY id DESC
LIMIT $2;
-- name: GetCommentWithCursor :many
SELECT id, content, signing, post_at
FROM comment
WHERE id <= $1 AND phase = 'ok' AND post_id = $2
ORDER BY id DESC
LIMIT $3;
-- name: InsertPostImage :exec
INSERT INTO media (url, post_id) VALUES ($1, $2);
-- name: InsertCommentImage :exec
INSERT INTO media (url, comment_id) VALUES ($1, $2);
-- name: AddPostHeart :one
UPDATE posts SET heart = heart + 1 WHERE id=$1 RETURNING heart;
-- name: AddCommentHeart :one
UPDATE comment SET heart = heart + 1 WHERE id=$1 RETURNING heart;
-- name: AdminGetPost :many
SELECT id, content, signing, post_at FROM posts WHERE phase = 'pending' ORDER BY id DESC LIMIT 100;
-- name: SuperAdminGetPost :many
SELECT id, content, signing, post_at, phase FROM posts WHERE phase = 'pending' OR phase = 'rejected' ORDER BY id DESC LIMIT 100;
-- name: AdminGetMedia :many
SELECT url from media WHERE post_id = $1;
-- name: AdminVerify :one
UPDATE posts SET phase = $1 WHERE id=$2 AND phase = 'pending' RETURNING id;
-- name: SuperAdminVerify :one
UPDATE posts SET phase = $1 WHERE id=$2 AND (phase = 'pending' OR phase = 'rejected') RETURNING id;
-- name: AdminUpdateImage :exec
UPDATE media SET visible = true WHERE post_id=$1;
-- name: AdminLoginGetTOTP :one
SELECT totp, super FROM admin WHERE username = $1 AND password = $2;
-- name: AdminCreateAccount :exec
INSERT INTO admin (username, password, totp) VALUES ($1, $2,$3);

37
sql/schema.sql Normal file
View file

@ -0,0 +1,37 @@
CREATE TYPE post_phase AS ENUM('pending', 'rejected', 'admin_rejected','ok', 'deleted');
CREATE TABLE posts (
id serial PRIMARY KEY,
content varchar(500) not null,
signing varchar(20),
hash char(64) UNIQUE NOT NULL,
post_at timestamp with time zone DEFAULT now(),
heart integer NOT NULL DEFAULT 0,
phase post_phase NOT NULL DEFAULT 'pending',
igid varchar(22)
);
CREATE TABLE comment (
id serial PRIMARY KEY,
post_id integer not null REFERENCES posts (id),
content varchar(200) not null,
signing varchar(20),
hash char(64) UNIQUE NOT NULL,
post_at timestamp with time zone DEFAULT now(),
heart integer NOT NULL DEFAULT 0
);
CREATE TABLE media (
url varchar(59) NOT NULL UNIQUE,
post_id integer REFERENCES posts (id),
comment_id integer REFERENCES comment (id),
visible boolean DEFAULT false,
CONSTRAINT uni_id CHECK ((post_id IS NULL) <> (comment_id IS NULL))
);
CREATE TABLE admin (
username varchar(20) NOT NULL UNIQUE PRIMARY KEY,
password char(45) NOT NULL,
totp char(33) NOT NULL,
super bool NOT NULL DEFAULT false
);

11
sqlc.yml Normal file
View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

View file

@ -1,26 +0,0 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}

View file

@ -1,34 +0,0 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}

View file

@ -1,15 +0,0 @@
import { insert_post, NewPost } from "@/db";
import { Blob } from "buffer";
import type { NextRequest } from "next/server";
export async function POST(req: NextRequest) {
let form = await req.formData();
let text = form.get("content")?.toString()
if (!text) {
return Response.error()
}
let request: NewPost = { content: text, image: [] };
let [id, hash] = await insert_post(request);
return Response.json({ id: id, hash: hash })
}

View file

@ -1,6 +0,0 @@
import { NextRequest } from "next/server";
export function Post(req: NextRequest) {
}

View file

@ -1,14 +0,0 @@
export default function Home() {
return (
<div className="relative min-h-screen px-8 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<div className="w-full h-5dvh fixed top-0 left-0">
<h1 className="text-4xl mt-4 ml-3"></h1>
</div>
<div className="h-[90dvh] w-[85dvw] absolute left-1/2 top-1/2 mt-5 -translate-1/2">
</div>
</div>
);
}

View file

@ -1,35 +0,0 @@
import { Suspense } from "react";
import Image from "next/image";
import { Attachment, MultiMediaType } from "@/db";
export default function Post(post_content: string, attachments: Attachment[]) {
let images = [];
let videos = [];
attachments.forEach(attachment => {
if (attachment.type == MultiMediaType.video) {
attachment.urls.forEach(url => {
videos.push(
<Suspense fallback={<p></p>}>
<video controls preload="none" aria-label="Video player">
<source src={url} type={attachment.type.toString()} />
Your browser does not support the video tag.
</video>
</Suspense>
)
})
} else if (attachment.type == MultiMediaType.image) {
attachment.urls.forEach(url => {
images.push(<Image src={url} alt="Uploaded" width={300} height={200}></Image>)
})
}
})
return (<div className="w-full h-fit">
<div>
{post_content}
</div>
<div>
<div></div>
</div>
</div>)
}

View file

@ -1,93 +0,0 @@
import { env, ReservedSQL, CryptoHasher, sql } from 'bun'
import { MIMEType } from 'util';
export enum MultiMediaType {
video, image
}
export interface Attachment {
urls: string[],
type: MultiMediaType
};
export interface Post {
post: string,
post_time: string,
hash: string,
attachments: Attachment[],
}
interface SQLPostCast {
hash: string, post: string, post_time: string, images: number[]
};
const SQLPostCast2Post = (obj: SQLPostCast) => {
let x: Post =
{
post: obj.post,
hash: obj.hash,
post_time: obj.post_time,
attachments: []
};
if (obj.images && obj.images.length > 0) {
x.attachments.push(
{
type: MultiMediaType.image, urls: obj.images.map(img => {
return `/img/${obj.hash}_${img}`
})
}
)
}
return x
}
export interface NewPost {
image: Blob[]
content: string,
}
export async function insert_post(post: NewPost): Promise<[number, string]> {
let post_hash = new CryptoHasher("sha256");
post_hash.update(post.content);
post_hash.update(Date.now().toString())
let populated = post_hash.digest("base64");
let [{ id, hash }] = await sql`INSERT INTO niming.posts (post, hash) VALUES (${post.content}, ${populated}) RETURNING id, hash`
return [id, hash]
}
export async function init_db() {
await sql.begin(async sql => {
await sql`CREATE SCHEMA niming`
await sql`CREATE TABLE niming.posts (id SERIAL PRIMARY KEY, hash char(44) UNIQUE, post VARCHAR(500), post_time TIMESTAMPTZ DEFAULT now())`
await sql`CREATE TABLE niming.images (id INTEGER PRIMARY KEY REFERENCES niming.posts (id), fileid integer[] )`
})
}
export class PostFetcher {
conn: Promise<ReservedSQL>;
constructor() {
this.conn = sql.reserve();
}
async init() {
await this.conn.then(async e => {
await e`DECLARE post_ptr CURSOR WITH HOLD FOR SELECT niming.posts.*, niming.images.fileid
AS images FROM niming.posts
LEFT JOIN niming.images ON niming.images.hash=niming.posts.hash ORDER BY niming.posts.post_time DESC, niming.posts.hash ASC`
}).catch(err => {
console.log(err);
})
}
async postgres_fetch() {
let x: SQLPostCast[] = []
if (this.conn) {
x = await (await this.conn)`FETCH 10 IN post_ptr`;
}
return x.map(e => {
return SQLPostCast2Post(e)
})
}
}

View file

@ -1,3 +0,0 @@
import { init_db } from "@/db";
init_db()

View file

@ -1,13 +0,0 @@
import { insert_post, NewPost } from '@/db';
import { faker } from '@faker-js/faker';
for (let i = 0; i < 100; i++) {
let text = faker.string.alpha(20);
let x: NewPost = { image: [], content: text };
let [p_id, p_hash] = await insert_post(x)
console.log(p_id, p_hash)
}

4
tools/insert_post.hurl Normal file
View file

@ -0,0 +1,4 @@
POST http://127.0.0.1:3000/api/post
[Multipart]
content: Test
signing: Test

View file

@ -1,13 +0,0 @@
import { PostFetcher, Post } from "@/db"
import { sql } from "bun";
let x: Post[] = [];
const fetcher = new PostFetcher();
await fetcher.init()
for (let i = 0; i < 10; i++) {
x = await fetcher.postgres_fetch()
console.log(x)
}

BIN
tools/test.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View file

@ -1,28 +0,0 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
},
"types":["bun-types"]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}