Compare commits
197 Commits
a96910edd2
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
24d04c3e79 | ||
|
e916eb2395 | ||
|
c24ed1e7e6 | ||
|
ae2c141efa | ||
|
15c513fe6d | ||
|
1c92ea36e3 | ||
|
9dacb152de | ||
|
9014f59a2c | ||
|
7294aa3e43 | ||
|
dd8f6d933e | ||
|
40f752259c | ||
|
e4844c3293 | ||
|
d304f3b319 | ||
|
eea9b85745 | ||
|
e3a4988ccf | ||
|
d22a435224 | ||
|
c4b64dfc09 | ||
|
672c006cd5 | ||
|
7566720420 | ||
|
c1d0b2f650 | ||
|
6d0caa1cde | ||
|
c558044646 | ||
|
acd23deed4 | ||
|
d010a9e5d4 | ||
|
6bedb3a17d | ||
|
bce86224bb | ||
|
47cced4e75 | ||
|
bf973b3b71 | ||
|
0756806f20 | ||
|
a8dbcfdb05 | ||
|
65cb542985 | ||
|
fabda61822 | ||
|
47aa2ae755 | ||
|
a268b7958c | ||
|
256c494ad0 | ||
|
2e18b2455b | ||
|
406eb9c93b | ||
|
2228f5054d | ||
|
397386be12 | ||
|
1975a89bbb | ||
|
f368e047be | ||
|
bfd3a0a1dd | ||
|
52f839ba9a | ||
|
3000b9d1a3 | ||
|
3d1c7e1bda | ||
|
5fae613da5 | ||
|
7733abde5d | ||
|
24783a3b66 | ||
|
c02c8cb610 | ||
|
85ff0c501a | ||
|
070cb8e12c | ||
|
364fda8a55 | ||
|
f50e376f6c | ||
|
8bf45bdefe | ||
|
a78fff8897 | ||
|
7a33018e44 | ||
|
17694d398b | ||
|
8d2cc9016f | ||
|
edc864d01e | ||
|
aac8a3eb54 | ||
|
e82f100b67 | ||
|
40ef26f75d | ||
|
519d570041 | ||
|
275fe80286 | ||
|
a75144d0db | ||
|
6c5324e131 | ||
|
ae48597f96 | ||
|
95d1284e68 | ||
|
c642f5ee44 | ||
|
6c60e250e1 | ||
|
a79995e7c0 | ||
|
fce42e4557 | ||
|
614509740b | ||
|
461d4c0e70 | ||
|
d9d6b81ac3 | ||
|
ed73b40bf5 | ||
|
2d05be679d | ||
|
06031620b7 | ||
|
cef4764d8c | ||
|
fce8669059 | ||
|
afb3b939e7 | ||
|
a3ef3d52d7 | ||
|
f958201097 | ||
|
fb4e48551b | ||
|
184fc3ecba | ||
|
ee16094855 | ||
|
f6482225ab | ||
|
e6d09ab41b | ||
|
5b78292d1b | ||
|
14e3750bd9 | ||
|
f962376f42 | ||
|
41ebb572fa | ||
|
56def6ea2c | ||
|
7044907a4a | ||
|
8ae33344eb | ||
|
75f5eb1456 | ||
|
b9a5bdb069 | ||
|
ea94fd7b54 | ||
|
57444f58f4 | ||
|
d7642c82bd | ||
|
e5c46a89df | ||
|
f97cb877f5 | ||
|
a8a3c8cc71 | ||
|
eb77ecbeb7 | ||
|
d19b5ab84f | ||
|
76889d23aa | ||
|
21e7c19fca | ||
|
dfa3b6b362 | ||
|
1bc687eba6 | ||
|
54739cd11e | ||
|
1867ff64f7 | ||
|
e15cd25c81 | ||
|
48c9bc21e0 | ||
|
7f0fc524da | ||
|
c00ee25050 | ||
|
47a5f037c8 | ||
|
583bec1619 | ||
|
4bd7f4dc8b | ||
|
262bac2a15 | ||
|
fda9b7517c | ||
|
09e670b096 | ||
|
096565f4e0 | ||
|
2486b317e3 | ||
|
7e03eeee38 | ||
|
768c809cbb | ||
ee7736b261 | |||
|
7d63123fd2 | ||
|
0fbb483301 | ||
|
b3fe2c2163 | ||
|
5b2fa00b1e | ||
|
edd02a03f7 | ||
|
ed9487a709 | ||
|
1308d4ad4c | ||
931a865c1c | |||
|
590adc8fc3 | ||
|
ba974b13be | ||
|
57f2aa518e | ||
|
44b5da1630 | ||
|
3812a7a505 | ||
|
f4a29faadf | ||
|
6e2d0b11d9 | ||
|
35d296b501 | ||
|
86d302d01e | ||
|
7c1ebe20c0 | ||
|
c883a52dd2 | ||
|
c8431281d7 | ||
|
9366f6e56e | ||
|
fa50baf927 | ||
|
40179a5557 | ||
|
f44a75d225 | ||
|
ee021f1422 | ||
|
0e0c717722 | ||
|
4da282012d | ||
|
bfd0c5e469 | ||
|
c9d3de0022 | ||
|
c366b1cbe3 | ||
|
25578f83f3 | ||
|
9b76311462 | ||
|
e129575eff | ||
|
a6373b3f87 | ||
|
f29df2e531 | ||
|
23450c0690 | ||
|
d03ad71064 | ||
|
8567ce7d17 | ||
|
9aa5dec54d | ||
|
b3b1ec4585 | ||
|
f38aab68cd | ||
|
18af7eae81 | ||
|
13fcc64f79 | ||
|
7e4ea47c61 | ||
|
32be79557a | ||
|
7c5c9d03d1 | ||
|
1603d9f438 | ||
|
d66b591f92 | ||
|
eb78e93cb2 | ||
|
f4ff82a804 | ||
|
485f72eded | ||
|
9e25e7e6cc | ||
|
e443448a94 | ||
|
3a6b51da97 | ||
|
e6407bbe4d | ||
|
3929f502ec | ||
|
e1365c4134 | ||
|
d7e7dc9bfc | ||
|
565d92020e | ||
|
88d3fbcf5c | ||
|
a8008c2c8c | ||
|
198583a7a3 | ||
|
b4f24f0ae0 | ||
|
fd611d7f67 | ||
|
6e73cf41a8 | ||
|
50f2d88abc | ||
|
476466f8c6 | ||
|
eaf074609e | ||
|
35b28d1003 | ||
|
1b68faf8ba | ||
|
066fc78b84 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/uploads
|
||||
/dist
|
||||
/keys
|
||||
|
||||
.DS_Store
|
||||
|
9
.idea/Messaging.iml
generated
Normal file
9
.idea/Messaging.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
19
.idea/dataSources.xml
generated
Normal file
19
.idea/dataSources.xml
generated
Normal file
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="hy_messaging@localhost" uuid="bdafe5e4-b64b-48b5-91b9-d004691610a6">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://localhost:5432/hy_messaging</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
<data-source source="LOCAL" name="messaging@im.solsynth.dev" uuid="4470fffd-ce14-4a9e-947a-5addec3a9ae5">
|
||||
<driver-ref>postgresql</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:postgresql://im.solsynth.dev:5432/messaging</jdbc-url>
|
||||
<working-dir>$ProjectFileDir$</working-dir>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Messaging.iml" filepath="$PROJECT_DIR$/.idea/Messaging.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/sqldialects.xml
generated
Normal file
6
.idea/sqldialects.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="SqlDialectMappings">
|
||||
<file url="PROJECT" dialect="PostgreSQL" />
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@@ -1,11 +1,9 @@
|
||||
# Building Backend
|
||||
FROM golang:alpine as messaging-server
|
||||
|
||||
RUN apk add nodejs npm
|
||||
|
||||
WORKDIR /source
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs -o /dist ./pkg/cmd/main.go
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -buildvcs -o /dist ./pkg/main.go
|
||||
|
||||
# Runtime
|
||||
FROM golang:alpine
|
||||
|
3
README.md
Normal file
3
README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Hypernet.Messaging
|
||||
|
||||
The instant message service in Hypernet universe.
|
18
ROADMAP.md
18
ROADMAP.md
@@ -1,18 +0,0 @@
|
||||
# Hydrogen.Messaging 规划
|
||||
|
||||
- [ ] 基本聊天功能
|
||||
- [ ] 发送文字消息
|
||||
- [ ] 发送带有附件的消息
|
||||
- [ ] 回复消息
|
||||
- [ ] 转发消息
|
||||
- [ ] 发送语音消息(特殊的带有附件的消息)
|
||||
- [ ] 发送位置信息(带有**元数据**的信息)
|
||||
- [ ] WebSocket 网关(一个 WS 链接完成所有的频道发送、接受消息功能)
|
||||
- [ ] 领域聊天功能(与 Hydrogen.Interactive 的联动)
|
||||
- [ ] 双方 GRPC 通信
|
||||
- [ ] 在广场的领域内置聊天频道显示
|
||||
- [ ] 在聊天的领域内显示广场的帖子节选
|
||||
- [ ] 双方共用一个领域系统,以广场的领域为准
|
||||
- [ ] 实时通讯功能
|
||||
- [ ] 支持实时通知
|
||||
- [ ] 支持音视频通话
|
155
go.mod
155
go.mod
@@ -1,79 +1,136 @@
|
||||
module git.solsynth.dev/hydrogen/messaging
|
||||
module git.solsynth.dev/hypernet/messaging
|
||||
|
||||
go 1.21.6
|
||||
go 1.23.2
|
||||
|
||||
require (
|
||||
git.solsynth.dev/hydrogen/identity v0.0.0-20240406034845-44d2ec9c4ace
|
||||
github.com/go-playground/validator/v10 v10.17.0
|
||||
github.com/gofiber/fiber/v2 v2.52.4
|
||||
github.com/gofiber/template/html/v2 v2.1.1
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
git.solsynth.dev/hypernet/nexus v0.0.0-20250329075932-d5422ab5b04c
|
||||
git.solsynth.dev/hypernet/paperclip v0.0.0-20250310151112-1d866f317f47
|
||||
git.solsynth.dev/hypernet/passport v0.0.0-20250315083747-32e91e26013c
|
||||
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88
|
||||
github.com/fatih/color v1.18.0
|
||||
github.com/go-playground/validator/v10 v10.22.1
|
||||
github.com/gofiber/fiber/v2 v2.52.6
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/rs/zerolog v1.31.0
|
||||
github.com/samber/lo v1.39.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
golang.org/x/crypto v0.21.0
|
||||
google.golang.org/grpc v1.61.1
|
||||
gorm.io/datatypes v1.2.0
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/gorm v1.25.6
|
||||
github.com/livekit/protocol v1.14.0
|
||||
github.com/livekit/server-sdk-go v1.1.8
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/samber/lo v1.47.0
|
||||
github.com/spf13/viper v1.19.0
|
||||
google.golang.org/grpc v1.70.0
|
||||
gorm.io/datatypes v1.2.4
|
||||
gorm.io/driver/postgres v1.5.9
|
||||
gorm.io/gorm v1.25.12
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/dgrr/websocket v0.1.1 // indirect
|
||||
github.com/fasthttp/websocket v1.5.8 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/benbjohnson/clock v1.3.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/eapache/channels v1.1.0 // indirect
|
||||
github.com/eapache/queue v1.1.0 // indirect
|
||||
github.com/eko/gocache/lib/v4 v4.2.0 // indirect
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.2 // indirect
|
||||
github.com/frostbyte73/core v0.0.10 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gammazero/deque v0.2.1 // indirect
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-sql-driver/mysql v1.7.1 // indirect
|
||||
github.com/gofiber/contrib/websocket v1.3.0 // indirect
|
||||
github.com/gofiber/template v1.8.3 // indirect
|
||||
github.com/gofiber/utils v1.1.0 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.1 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/jxskiss/base62 v1.1.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0 // indirect
|
||||
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 // indirect
|
||||
github.com/livekit/mediatransportutil v0.0.0-20231213075826-cccbf2b93d3f // indirect
|
||||
github.com/livekit/psrpc v0.5.3-0.20240228172457-3724cb4adbc4 // indirect
|
||||
github.com/magefile/mage v1.15.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // 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.15 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/philhofer/fwd v1.1.2 // indirect
|
||||
github.com/nats-io/nats.go v1.37.0 // indirect
|
||||
github.com/nats-io/nkeys v0.4.7 // indirect
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.9 // indirect
|
||||
github.com/pion/ice/v2 v2.3.13 // indirect
|
||||
github.com/pion/interceptor v0.1.25 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/rtcp v1.2.13 // indirect
|
||||
github.com/pion/rtp v1.8.3 // indirect
|
||||
github.com/pion/sctp v1.8.12 // indirect
|
||||
github.com/pion/sdp/v3 v3.0.6 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.4 // indirect
|
||||
github.com/pion/turn/v2 v2.1.3 // indirect
|
||||
github.com/pion/webrtc/v3 v3.2.28 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.19.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.52.3 // indirect
|
||||
github.com/prometheus/procfs v0.13.0 // indirect
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect
|
||||
github.com/redis/go-redis/v9 v9.7.3 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.6.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.1.8 // indirect
|
||||
github.com/thoas/go-funk v0.9.3 // indirect
|
||||
github.com/tinylib/msgp v1.2.5 // indirect
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.52.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.59.0 // indirect
|
||||
github.com/zeebo/xxh3 v1.0.2 // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sync v0.5.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
go.uber.org/zap/exp v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/mysql v1.5.2 // indirect
|
||||
gorm.io/driver/mysql v1.5.7 // indirect
|
||||
)
|
||||
|
509
go.sum
509
go.sum
@@ -1,129 +1,172 @@
|
||||
git.solsynth.dev/hydrogen/identity v0.0.0-20240320131628-6ac77f36957f h1:6vRGU5bSb7Za6HUhpX1bYMsKElRbUyL/qB5fAs5IDE0=
|
||||
git.solsynth.dev/hydrogen/identity v0.0.0-20240320131628-6ac77f36957f/go.mod h1:rmh5biOQLvoIE2iRFbOfD0TITMP1orYpqzhUw50Z4ck=
|
||||
git.solsynth.dev/hydrogen/identity v0.0.0-20240331080359-e8aac7bb6627 h1:BUhDqy/Whw1yVbDpmGk7clzRAC1jo+AOsCwuwhCVwkg=
|
||||
git.solsynth.dev/hydrogen/identity v0.0.0-20240331080359-e8aac7bb6627/go.mod h1:GxcduEpQWQ2mO37A9uRtseS680uMLi957GDywRBAJHg=
|
||||
git.solsynth.dev/hydrogen/identity v0.0.0-20240406034845-44d2ec9c4ace h1:bXbBjM56vA3BxfyuD0IrlJabpVx5bLi4qCv3/RsPa1c=
|
||||
git.solsynth.dev/hydrogen/identity v0.0.0-20240406034845-44d2ec9c4ace/go.mod h1:GxcduEpQWQ2mO37A9uRtseS680uMLi957GDywRBAJHg=
|
||||
github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
|
||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
git.solsynth.dev/hypernet/nexus v0.0.0-20250329053929-488793a2dc56 h1:SnT9NVcXQ1WDka9kKAA+lH/r2UJouND7FDugu4ZZwLc=
|
||||
git.solsynth.dev/hypernet/nexus v0.0.0-20250329053929-488793a2dc56/go.mod h1:5tk62VQ1DcbR0EAN2jAOqYxHiegUPEC805JlfQ/G19I=
|
||||
git.solsynth.dev/hypernet/nexus v0.0.0-20250329075932-d5422ab5b04c h1:XgdTgJxSAQuCbiG15hN5pY6chzcz8sX3Onm2itS+Ufs=
|
||||
git.solsynth.dev/hypernet/nexus v0.0.0-20250329075932-d5422ab5b04c/go.mod h1:5tk62VQ1DcbR0EAN2jAOqYxHiegUPEC805JlfQ/G19I=
|
||||
git.solsynth.dev/hypernet/paperclip v0.0.0-20250310151112-1d866f317f47 h1:fvu+bNKPTNtQocssnKbEZ66MqR0iBfAxY3HwlqnmYyE=
|
||||
git.solsynth.dev/hypernet/paperclip v0.0.0-20250310151112-1d866f317f47/go.mod h1:jvxq2qftz2v72x+24+cTFJdQKr9eHQTdk3KVR7cx36s=
|
||||
git.solsynth.dev/hypernet/passport v0.0.0-20250315083747-32e91e26013c h1:XB8EBX34WB2skmjaVFot5IlxKF2qFZ2SueG/Y9SiJ6Y=
|
||||
git.solsynth.dev/hypernet/passport v0.0.0-20250315083747-32e91e26013c/go.mod h1:k7MZQWYBpxlk3g9bx0HTh5C3m+MG/wr0hAiRM/VyAqs=
|
||||
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88 h1:2HEENe9KUrdaJeNBzx9lsuXQGyzWqCgnLTKQnr8xFr8=
|
||||
git.solsynth.dev/hypernet/pusher v0.0.0-20250216145944-5fb769823a88/go.mod h1:ildzMtLagNsLK0Rkw4Hgk2TrrwqZnjwJIUx0MNZwcDY=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o=
|
||||
github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrr/websocket v0.1.1 h1:fg6irjiUyGRmqzJ1vwy3vRPv6PlIO7+eF4Q7oi+WDAo=
|
||||
github.com/dgrr/websocket v0.1.1/go.mod h1:d30hG8q3dQuz6eSwROXzIodSvPTNi52j1VvxrK7RWXc=
|
||||
github.com/fasthttp/websocket v1.5.8 h1:k5DpirKkftIF/w1R8ZzjSgARJrs54Je9YJK37DL/Ah8=
|
||||
github.com/fasthttp/websocket v1.5.8/go.mod h1:d08g8WaT6nnyvg9uMm8K9zMYyDjfKyj3170AtPRuVU0=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/eapache/channels v1.1.0 h1:F1taHcn7/F0i8DYqKXJnyhJcVpp2kgFcNePxXtnyu4k=
|
||||
github.com/eapache/channels v1.1.0/go.mod h1:jMm2qB5Ubtg9zLd+inMZd2/NUvXgzmWXsDaLyQIGfH0=
|
||||
github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc=
|
||||
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
|
||||
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
|
||||
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.2 h1:Thw31fzGuH3WzJywsdbMivOmP550D6JS7GDHhvCJPA0=
|
||||
github.com/eko/gocache/store/redis/v4 v4.2.2/go.mod h1:LaTxLKx9TG/YUEybQvPMij++D7PBTIJ4+pzvk0ykz0w=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/frostbyte73/core v0.0.10 h1:D4DQXdPb8ICayz0n75rs4UYTXrUSdxzUfeleuNJORsU=
|
||||
github.com/frostbyte73/core v0.0.10/go.mod h1:XsOGqrqe/VEV7+8vJ+3a8qnCIXNbKsoEiu/czs7nrcU=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gammazero/deque v0.2.1 h1:qSdsbG6pgp6nL7A0+K/B7s12mcCY/5l5SIUpMOl+dC0=
|
||||
github.com/gammazero/deque v0.2.1/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
|
||||
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
|
||||
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
|
||||
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI=
|
||||
github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
|
||||
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/gobwas/ws v1.0.4/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofiber/contrib/websocket v1.3.0 h1:XADFAGorer1VJ1bqC4UkCjqS37kwRTV0415+050NrMk=
|
||||
github.com/gofiber/contrib/websocket v1.3.0/go.mod h1:xguaOzn2ZZ759LavtosEP+rcxIgBEE/rdumPINhR+Xo=
|
||||
github.com/gofiber/fiber/v2 v2.52.0 h1:S+qXi7y+/Pgvqq4DrSmREGiFwtB7Bu6+QFLuIHYw/UE=
|
||||
github.com/gofiber/fiber/v2 v2.52.0/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gofiber/fiber/v2 v2.52.4 h1:P+T+4iK7VaqUsq2PALYEfBBo6bJZ4q3FP8cZ84EggTM=
|
||||
github.com/gofiber/fiber/v2 v2.52.4/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
||||
github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
|
||||
github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
|
||||
github.com/gofiber/template/html/v2 v2.1.1 h1:QEy3O3EBkvwDthy5bXVGUseOyO6ldJoiDxlF4+MJiV8=
|
||||
github.com/gofiber/template/html/v2 v2.1.1/go.mod h1:2G0GHHOUx70C1LDncoBpe4T6maQbNa4x1CVNFW0wju0=
|
||||
github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
|
||||
github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
|
||||
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
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.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
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/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
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-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI=
|
||||
github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
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/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.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs=
|
||||
github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA=
|
||||
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/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
|
||||
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/jxskiss/base62 v1.1.0 h1:A5zbF8v8WXx2xixnAKD2w+abC+sIzYJX+nxmhA6HWFw=
|
||||
github.com/jxskiss/base62 v1.1.0/go.mod h1:HhWAlUXvxKThfOlZbcuFzsqwtF5TcqS9ru3y5GfjWAc=
|
||||
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/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0 h1:QRbbVkfgNippHOS8PXDkti4NaWeyYfcBTHtw7k08o4c=
|
||||
github.com/lithammer/shortuuid/v4 v4.0.0/go.mod h1:Zs8puNcrvf2rV9rTH51ZLLcj7ZXqQI3lv67aw4KiB1Y=
|
||||
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 h1:jm09419p0lqTkDaKb5iXdynYrzB84ErPPO4LbRASk58=
|
||||
github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1/go.mod h1:Rs3MhFwutWhGwmY1VQsygw28z5bWcnEYmS1OG9OxjOQ=
|
||||
github.com/livekit/mediatransportutil v0.0.0-20231213075826-cccbf2b93d3f h1:XHrwGwLNGQB3ZqolH1YdMH/22hgXKr4vm+2M7JKMMGg=
|
||||
github.com/livekit/mediatransportutil v0.0.0-20231213075826-cccbf2b93d3f/go.mod h1:GBzn9xL+mivI1pW+tyExcKgbc0VOc29I9yJsNcAVaAc=
|
||||
github.com/livekit/protocol v1.14.0 h1:W0i5HR2Efoy2j9NhmONolU3FIsUDVl6MAT5zWfALev0=
|
||||
github.com/livekit/protocol v1.14.0/go.mod h1:pnn0Dv+/0K0OFqKHX6J6SreYO1dZxl6tDuAZ1ns8L/w=
|
||||
github.com/livekit/psrpc v0.5.3-0.20240228172457-3724cb4adbc4 h1:253WtQ2VGVHzIIzW9MUZj7vUDDILESU3zsEbiRdxYF0=
|
||||
github.com/livekit/psrpc v0.5.3-0.20240228172457-3724cb4adbc4/go.mod h1:CQUBSPfYYAaevg1TNCc6/aYsa8DJH4jSRFdCeSZk5u0=
|
||||
github.com/livekit/server-sdk-go v1.1.8 h1:7yIZWrZKCK23R5hz0UeQXbPHcnpz0HmDF9Yl9/8rNog=
|
||||
github.com/livekit/server-sdk-go v1.1.8/go.mod h1:S2ebe9gCFX14bAPjXNJ+4i53N/jpMHRww1OUoMn28xc=
|
||||
github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg=
|
||||
github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
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.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
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.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
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.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
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/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/microsoft/go-mssqldb v0.17.0 h1:Fto83dMZPnYv1Zwx5vHHxpNraeEaUlQ/hhHLgZiaenE=
|
||||
@@ -133,161 +176,317 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw=
|
||||
github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0=
|
||||
github.com/nats-io/nats.go v1.37.0 h1:07rauXbVnnJvv1gfIyghFEo6lUcYRY0WXc3x7x0vUxE=
|
||||
github.com/nats-io/nats.go v1.37.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8=
|
||||
github.com/nats-io/nkeys v0.4.7 h1:RwNJbbIdYCoClSDNY7QVKZlyb/wfT6ugvFCiKy6vDvI=
|
||||
github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc=
|
||||
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
|
||||
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/dtls/v2 v2.2.9 h1:K+D/aVf9/REahQvqk6G5JavdrD8W1PWDKC11UlwN7ts=
|
||||
github.com/pion/dtls/v2 v2.2.9/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/ice/v2 v2.3.13 h1:xOxP+4V9nSDlUaGFRf/LvAuGHDXRcjIdsbbXPK/w7c8=
|
||||
github.com/pion/ice/v2 v2.3.13/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=
|
||||
github.com/pion/interceptor v0.1.25 h1:pwY9r7P6ToQ3+IF0bajN0xmk/fNw/suTgaTdlwTDmhc=
|
||||
github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=
|
||||
github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtcp v1.2.13 h1:+EQijuisKwm/8VBs8nWllr0bIndR7Lf7cZG200mpbNo=
|
||||
github.com/pion/rtcp v1.2.13/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=
|
||||
github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
|
||||
github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.12 h1:2VX50pedElH+is6FI+OKyRTeN5oy4mrk2HjnGa3UCmY=
|
||||
github.com/pion/sctp v1.8.12/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=
|
||||
github.com/pion/sdp/v3 v3.0.6 h1:WuDLhtuFUUVpTfus9ILC4HRyHsW6TdugjEX/QY9OiUw=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=
|
||||
github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=
|
||||
github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=
|
||||
github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
|
||||
github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
|
||||
github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
|
||||
github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
|
||||
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
|
||||
github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA=
|
||||
github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
|
||||
github.com/pion/webrtc/v3 v3.2.28 h1:ienStxZ6HcjtH2UlmnFpMM0loENiYjaX437uIUpQSKo=
|
||||
github.com/pion/webrtc/v3 v3.2.28/go.mod h1:PNRCEuQlibrmuBhOTnol9j6KkIbUG11aHLEfNpUYey0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||
github.com/prometheus/common v0.52.3 h1:5f8uj6ZwHSscOGNdIQg6OiZv/ybiK2CO2q2drVZAQSA=
|
||||
github.com/prometheus/common v0.52.3/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0 h1:EewKT7/LNac5SLiEblJeUu8z5eERHrmRLnMQL2d7qX4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.1.0/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
|
||||
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
|
||||
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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
|
||||
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
|
||||
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
|
||||
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511 h1:KanIMPX0QdEdB4R3CiimCAbxFrhB3j7h0/OvpYGVQa8=
|
||||
github.com/savsgio/gotils v0.0.0-20240303185622-093b76447511/go.mod h1:sM7Mt7uEoCeFSCBM+qBrqvEo+/9vdmj19wzp3yzUhmg=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
|
||||
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
|
||||
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
|
||||
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/thoas/go-funk v0.9.3 h1:7+nAEx3kn5ZJcnDm2Bh23N2yOtweO14bi//dvRtgLpw=
|
||||
github.com/thoas/go-funk v0.9.3/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q=
|
||||
github.com/tinylib/msgp v1.2.5 h1:WeQg1whrXRFiZusidTQqzETkRpGjFjcIhW6uqWH09po=
|
||||
github.com/tinylib/msgp v1.2.5/go.mod h1:ykjzy2wzgrlvpDCRc4LA8UXy6D8bzMSuAF3WD57Gok0=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU=
|
||||
github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A=
|
||||
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.28.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||
github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8=
|
||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||
github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
|
||||
github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
|
||||
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
|
||||
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
|
||||
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
|
||||
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
|
||||
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
|
||||
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
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/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
|
||||
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||
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=
|
||||
go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs=
|
||||
go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
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-20231219180239-dc181d75b848 h1:+iq7lrkxmFNBM7xx+Rae2W6uyPfhPeDWD+n+JgppptE=
|
||||
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
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/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
|
||||
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
|
||||
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||
google.golang.org/grpc v1.61.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
|
||||
google.golang.org/grpc v1.61.1/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287 h1:J1H9f+LEdWAfHcez/4cvaVBox7cOYT+IU6rgqj5x++8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250127172529-29210b9bc287/go.mod h1:8BS3B93F/U1juMFq9+EDk+qOT5CO1R9IzXxG3PTqiRk=
|
||||
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
|
||||
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
|
||||
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.36.4 h1:6A3ZDJHn/eNqc1i+IdefRzy/9PokBTPvcqMySR7NNIM=
|
||||
google.golang.org/protobuf v1.36.4/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
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=
|
||||
gorm.io/datatypes v1.2.0 h1:5YT+eokWdIxhJgWHdrb2zYUimyk0+TaFth+7a0ybzco=
|
||||
gorm.io/datatypes v1.2.0/go.mod h1:o1dh0ZvjIjhH/bngTpypG6lVRJ5chTBxE09FH/71k04=
|
||||
gorm.io/driver/mysql v1.5.2 h1:QC2HRskSE75wBuOxe0+iCkyJZ+RqpudsQtqkp+IMuXs=
|
||||
gorm.io/driver/mysql v1.5.2/go.mod h1:pQLhh1Ut/WUAySdTHwBpBv6+JKcj+ua4ZFx1QQTBzb8=
|
||||
gorm.io/driver/postgres v1.5.4 h1:Iyrp9Meh3GmbSuyIAGyjkN+n9K+GHX9b9MqsTL4EJCo=
|
||||
gorm.io/driver/postgres v1.5.4/go.mod h1:Bgo89+h0CRcdA33Y6frlaHHVuTdOf87pmyzwW9C/BH0=
|
||||
gorm.io/datatypes v1.2.4 h1:uZmGAcK/QZ0uyfCuVg0VQY1ZmV9h1fuG0tMwKByO1z4=
|
||||
gorm.io/datatypes v1.2.4/go.mod h1:f4BsLcFAX67szSv8svwLRjklArSHAvHLeE3pXAS5DZI=
|
||||
gorm.io/driver/mysql v1.5.7 h1:MndhOPYOfEp2rHKgkZIhJ16eVUIRf2HmzgoPmh7FCWo=
|
||||
gorm.io/driver/mysql v1.5.7/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.5.9 h1:DkegyItji119OlcaLjqN11kHoUgZ/j13E0jkJZgD6A8=
|
||||
gorm.io/driver/postgres v1.5.9/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
|
||||
gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU=
|
||||
gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
|
||||
gorm.io/driver/sqlserver v1.4.1 h1:t4r4r6Jam5E6ejqP7N82qAJIJAht27EGT41HyPfXRw0=
|
||||
gorm.io/driver/sqlserver v1.4.1/go.mod h1:DJ4P+MeZbc5rvY58PnmN1Lnyvb5gw5NPzGshHDnJLig=
|
||||
gorm.io/gorm v1.25.2-0.20230530020048-26663ab9bf55/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
|
||||
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
|
||||
|
@@ -1,70 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"github.com/robfig/cron/v3"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/grpc"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/server"
|
||||
|
||||
messaging "git.solsynth.dev/hydrogen/messaging/pkg"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Configure settings
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("..")
|
||||
viper.SetConfigName("settings")
|
||||
viper.SetConfigType("toml")
|
||||
|
||||
// Load settings
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Panic().Err(err).Msg("An error occurred when loading settings.")
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
if err := database.NewSource(); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when connect to database.")
|
||||
} else if err := database.RunMigration(database.C); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when running database auto migration.")
|
||||
}
|
||||
|
||||
// Connect other services
|
||||
go func() {
|
||||
if err := grpc.ConnectPassport(); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when connecting to identity grpc endpoint...")
|
||||
}
|
||||
}()
|
||||
|
||||
// Server
|
||||
server.NewServer()
|
||||
go server.Listen()
|
||||
|
||||
// Configure timed tasks
|
||||
quartz := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(&log.Logger)))
|
||||
quartz.AddFunc("@every 60m", services.DoAutoDatabaseCleanup)
|
||||
quartz.Start()
|
||||
|
||||
// Messages
|
||||
log.Info().Msgf("Messaging v%s is started...", messaging.AppVersion)
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Info().Msgf("Messaging v%s is quitting...", messaging.AppVersion)
|
||||
|
||||
quartz.Stop()
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var DatabaseAutoActionRange = []any{
|
||||
&models.Account{},
|
||||
&models.Channel{},
|
||||
&models.ChannelMember{},
|
||||
&models.Message{},
|
||||
&models.Attachment{},
|
||||
}
|
||||
|
||||
func RunMigration(source *gorm.DB) error {
|
||||
if err := source.AutoMigrate(DatabaseAutoActionRange...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"gorm.io/gorm/schema"
|
||||
)
|
||||
|
||||
var C *gorm.DB
|
||||
|
||||
func NewSource() error {
|
||||
var err error
|
||||
|
||||
dialector := postgres.Open(viper.GetString("database.dsn"))
|
||||
C, err = gorm.Open(dialector, &gorm.Config{NamingStrategy: schema.NamingStrategy{
|
||||
TablePrefix: viper.GetString("database.prefix"),
|
||||
}, Logger: logger.New(&log.Logger, logger.Config{
|
||||
Colorful: true,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
LogLevel: lo.Ternary(viper.GetBool("debug.database"), logger.Info, logger.Silent),
|
||||
})})
|
||||
|
||||
return err
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
package pkg
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed views/*
|
||||
var FS embed.FS
|
@@ -1,26 +0,0 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
idpb "git.solsynth.dev/hydrogen/identity/pkg/grpc/proto"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var Friendships idpb.FriendshipsClient
|
||||
var Notify idpb.NotifyClient
|
||||
var Auth idpb.AuthClient
|
||||
|
||||
func ConnectPassport() error {
|
||||
addr := viper.GetString("identity.grpc_endpoint")
|
||||
if conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())); err != nil {
|
||||
return err
|
||||
} else {
|
||||
Friendships = idpb.NewFriendshipsClient(conn)
|
||||
Notify = idpb.NewNotifyClient(conn)
|
||||
Auth = idpb.NewAuthClient(conn)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
23
pkg/internal/database/migrator.go
Normal file
23
pkg/internal/database/migrator.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var AutoMaintainRange = []any{
|
||||
&authm.Realm{},
|
||||
&models.Channel{},
|
||||
&models.ChannelMember{},
|
||||
&models.Call{},
|
||||
&models.Event{},
|
||||
}
|
||||
|
||||
func RunMigration(source *gorm.DB) error {
|
||||
if err := source.AutoMigrate(AutoMaintainRange...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
31
pkg/internal/database/source.go
Normal file
31
pkg/internal/database/source.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var C *gorm.DB
|
||||
|
||||
func NewGorm() error {
|
||||
dsn, err := cruda.NewCrudaConn(gap.Nx).AllocDatabase("messaging")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to alloc database from nexus: %v", err)
|
||||
}
|
||||
|
||||
C, err = gorm.Open(postgres.Open(dsn), &gorm.Config{Logger: logger.New(&log.Logger, logger.Config{
|
||||
Colorful: true,
|
||||
IgnoreRecordNotFoundError: true,
|
||||
LogLevel: lo.Ternary(viper.GetBool("debug.database"), logger.Info, logger.Silent),
|
||||
}), DisableForeignKeyConstraintWhenMigrating: true})
|
||||
|
||||
return err
|
||||
|
||||
}
|
62
pkg/internal/gap/server.go
Normal file
62
pkg/internal/gap/server.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package gap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/proto"
|
||||
"git.solsynth.dev/hypernet/pusher/pkg/pushkit/pushcon"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var (
|
||||
Nx *nex.Conn
|
||||
Px *pushcon.Conn
|
||||
Ca *cachekit.Conn
|
||||
)
|
||||
|
||||
func InitializeToNexus() error {
|
||||
grpcBind := strings.SplitN(viper.GetString("grpc_bind"), ":", 2)
|
||||
httpBind := strings.SplitN(viper.GetString("bind"), ":", 2)
|
||||
|
||||
outboundIp, _ := nex.GetOutboundIP()
|
||||
|
||||
grpcOutbound := fmt.Sprintf("%s:%s", outboundIp, grpcBind[1])
|
||||
httpOutbound := fmt.Sprintf("%s:%s", outboundIp, httpBind[1])
|
||||
|
||||
var err error
|
||||
Nx, err = nex.NewNexusConn(viper.GetString("nexus_addr"), &proto.ServiceInfo{
|
||||
Id: viper.GetString("id"),
|
||||
Type: "im",
|
||||
Label: "Messaging",
|
||||
GrpcAddr: grpcOutbound,
|
||||
HttpAddr: lo.ToPtr("http://" + httpOutbound + "/api"),
|
||||
})
|
||||
if err == nil {
|
||||
go func() {
|
||||
err := Nx.RunRegistering()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("An error occurred while registering service...")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
Px, err = pushcon.NewConn(Nx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during initialize pushcon: %v", err)
|
||||
}
|
||||
|
||||
Ca, err = cachekit.NewConn(Nx, 3*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error during initialize cachekit: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
27
pkg/internal/grpc/health.go
Normal file
27
pkg/internal/grpc/health.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
health "google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
func (v *Server) Check(ctx context.Context, request *health.HealthCheckRequest) (*health.HealthCheckResponse, error) {
|
||||
return &health.HealthCheckResponse{
|
||||
Status: health.HealthCheckResponse_SERVING,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *Server) Watch(request *health.HealthCheckRequest, server health.Health_WatchServer) error {
|
||||
for {
|
||||
if server.Send(&health.HealthCheckResponse{
|
||||
Status: health.HealthCheckResponse_SERVING,
|
||||
}) != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(1000 * time.Millisecond)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
41
pkg/internal/grpc/server.go
Normal file
41
pkg/internal/grpc/server.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/proto"
|
||||
"github.com/spf13/viper"
|
||||
"google.golang.org/grpc"
|
||||
health "google.golang.org/grpc/health/grpc_health_v1"
|
||||
"google.golang.org/grpc/reflection"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
proto.UnimplementedStreamServiceServer
|
||||
proto.UnimplementedDirectoryServiceServer
|
||||
|
||||
srv *grpc.Server
|
||||
}
|
||||
|
||||
func NewGrpc() *Server {
|
||||
server := &Server{
|
||||
srv: grpc.NewServer(),
|
||||
}
|
||||
|
||||
health.RegisterHealthServer(server.srv, server)
|
||||
proto.RegisterStreamServiceServer(server.srv, server)
|
||||
proto.RegisterDirectoryServiceServer(server.srv, server)
|
||||
|
||||
reflection.Register(server.srv)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
func (v *Server) Listen() error {
|
||||
listener, err := net.Listen("tcp", viper.GetString("grpc_bind"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return v.srv.Serve(listener)
|
||||
}
|
69
pkg/internal/grpc/services.go
Normal file
69
pkg/internal/grpc/services.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/proto"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
)
|
||||
|
||||
func (v *Server) BroadcastEvent(ctx context.Context, in *proto.EventInfo) (*proto.EventResponse, error) {
|
||||
log.Debug().Str("event", in.GetEvent()).
|
||||
Msg("Got a broadcasting event...")
|
||||
|
||||
switch in.GetEvent() {
|
||||
// Clear the subscribed channel
|
||||
case "ws.client.unregister":
|
||||
// Update user last seen at
|
||||
data := nex.DecodeMap(in.GetData())
|
||||
id := data["id"].(string)
|
||||
services.UnsubscribeAllWithClient(id)
|
||||
log.Info().Str("client", id).Msg("Client unregistered, cleaning up subscribed channels...")
|
||||
// Account recycle
|
||||
case "deletion":
|
||||
data := nex.DecodeMap(in.GetData())
|
||||
resType, ok := data["type"].(string)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch resType {
|
||||
case "account":
|
||||
var data struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
if err := jsoniter.Unmarshal(in.GetData(), &data); err != nil {
|
||||
break
|
||||
}
|
||||
tx := database.C.Begin()
|
||||
for _, model := range database.AutoMaintainRange {
|
||||
switch model.(type) {
|
||||
default:
|
||||
tx.Delete(model, "account_id = ?", data.ID)
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
case "realm":
|
||||
var data struct {
|
||||
ID int `json:"id"`
|
||||
}
|
||||
if err := jsoniter.Unmarshal(in.GetData(), &data); err != nil {
|
||||
break
|
||||
}
|
||||
var channels []models.Channel
|
||||
if err := database.C.Where("realm_id = ?", data.ID).Find(&channels).Error; err != nil {
|
||||
break
|
||||
}
|
||||
for _, channel := range channels {
|
||||
_ = services.DeleteChannel(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &proto.EventResponse{}, nil
|
||||
}
|
111
pkg/internal/grpc/stream.go
Normal file
111
pkg/internal/grpc/stream.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package grpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/proto"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func (v *Server) PushStream(_ context.Context, request *proto.PushStreamRequest) (*proto.PushStreamResponse, error) {
|
||||
sc := proto.NewStreamServiceClient(gap.Nx.GetNexusGrpcConn())
|
||||
|
||||
var in nex.WebSocketPackage
|
||||
if err := jsoniter.Unmarshal(request.GetBody(), &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch in.Action {
|
||||
case "status.typing":
|
||||
var data struct {
|
||||
ChannelID uint `json:"channel_id" validate:"required"`
|
||||
}
|
||||
|
||||
err := jsoniter.Unmarshal(in.RawPayload(), &data)
|
||||
if err == nil {
|
||||
err = exts.ValidateStruct(data)
|
||||
}
|
||||
if err != nil {
|
||||
_, _ = sc.PushStream(context.Background(), &proto.PushStreamRequest{
|
||||
ClientId: request.ClientId,
|
||||
Body: nex.WebSocketPackage{
|
||||
Action: "error",
|
||||
Message: fmt.Sprintf("unable parse payload: %v", err),
|
||||
}.Marshal(),
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
err = services.SetTypingStatus(data.ChannelID, uint(request.GetUserId()))
|
||||
if err != nil {
|
||||
_, _ = sc.PushStream(context.Background(), &proto.PushStreamRequest{
|
||||
ClientId: request.ClientId,
|
||||
Body: nex.WebSocketPackage{
|
||||
Action: "error",
|
||||
Message: fmt.Sprintf("unable boardcast status: %v", err),
|
||||
}.Marshal(),
|
||||
})
|
||||
break
|
||||
}
|
||||
case "events.subscribe", "events.unsubscribe", "events.unsubscribeAll":
|
||||
var data struct {
|
||||
ChannelID uint `json:"channel_id" validate:"required"`
|
||||
}
|
||||
|
||||
err := jsoniter.Unmarshal(in.RawPayload(), &data)
|
||||
if err == nil {
|
||||
err = exts.ValidateStruct(data)
|
||||
}
|
||||
if err != nil {
|
||||
_, _ = sc.PushStream(context.Background(), &proto.PushStreamRequest{
|
||||
ClientId: request.ClientId,
|
||||
Body: nex.WebSocketPackage{
|
||||
Action: "error",
|
||||
Message: fmt.Sprintf("unable parse payload: %v", err),
|
||||
}.Marshal(),
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
action := strings.Split(in.Action, ".")[1]
|
||||
switch action {
|
||||
case "subscribe":
|
||||
services.SubscribeChannel(uint(request.GetUserId()), data.ChannelID, request.GetClientId())
|
||||
case "unsubscribe":
|
||||
services.UnsubscribeChannel(uint(request.GetUserId()), data.ChannelID)
|
||||
case "unsubscribeAll":
|
||||
services.UnsubscribeAll(uint(request.GetUserId()))
|
||||
}
|
||||
case "events.read":
|
||||
var data struct {
|
||||
ChannelMemberID uint `json:"channel_member_id" validate:"required"`
|
||||
EventID uint `json:"event_id" validate:"required"`
|
||||
}
|
||||
|
||||
err := jsoniter.Unmarshal(in.RawPayload(), &data)
|
||||
if err == nil {
|
||||
err = exts.ValidateStruct(data)
|
||||
}
|
||||
if err != nil {
|
||||
_, _ = sc.PushStream(context.Background(), &proto.PushStreamRequest{
|
||||
ClientId: request.ClientId,
|
||||
Body: nex.WebSocketPackage{
|
||||
Action: "error",
|
||||
Message: fmt.Sprintf("unable parse payload: %v", err),
|
||||
}.Marshal(),
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// WARN We trust the user here, so we don't need to check if the channel member is valid for performance
|
||||
services.SetReadingAnchor(data.ChannelMemberID, data.EventID)
|
||||
}
|
||||
|
||||
return &proto.PushStreamResponse{}, nil
|
||||
}
|
21
pkg/internal/models/calls.go
Normal file
21
pkg/internal/models/calls.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Call struct {
|
||||
cruda.BaseModel
|
||||
|
||||
EndedAt *time.Time `json:"ended_at"`
|
||||
|
||||
ExternalID string `json:"external_id"`
|
||||
FounderID uint `json:"founder_id"`
|
||||
ChannelID uint `json:"channel_id"`
|
||||
Founder ChannelMember `json:"founder"`
|
||||
Channel Channel `json:"channel"`
|
||||
|
||||
Participants []*livekit.ParticipantInfo `json:"participants" gorm:"-"`
|
||||
}
|
69
pkg/internal/models/channels.go
Normal file
69
pkg/internal/models/channels.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
)
|
||||
|
||||
type ChannelType = uint8
|
||||
|
||||
const (
|
||||
ChannelTypeCommon = ChannelType(iota)
|
||||
ChannelTypeDirect
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
cruda.BaseModel
|
||||
|
||||
Alias string `json:"alias"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Members []ChannelMember `json:"members"`
|
||||
Messages []Event `json:"messages"`
|
||||
Calls []Call `json:"calls"`
|
||||
Type ChannelType `json:"type"`
|
||||
AccountID uint `json:"account_id"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsCommunity bool `json:"is_community"`
|
||||
|
||||
Realm *authm.Realm `json:"realm" gorm:"-"`
|
||||
RealmID *uint `json:"realm_id"`
|
||||
}
|
||||
|
||||
func (v Channel) DisplayText() string {
|
||||
if v.Type == ChannelTypeDirect {
|
||||
return "DM"
|
||||
}
|
||||
if v.Realm != nil {
|
||||
return fmt.Sprintf("%s, %s", v.Name, v.Realm.Name)
|
||||
}
|
||||
return v.Name
|
||||
}
|
||||
|
||||
type NotifyLevel = int8
|
||||
|
||||
const (
|
||||
NotifyLevelAll = NotifyLevel(iota)
|
||||
NotifyLevelMentioned
|
||||
NotifyLevelNone
|
||||
)
|
||||
|
||||
type ChannelMember struct {
|
||||
cruda.BaseModel
|
||||
|
||||
Name string `json:"name"`
|
||||
Nick string `json:"nick"`
|
||||
Avatar *string `json:"avatar"`
|
||||
|
||||
ChannelID uint `json:"channel_id"`
|
||||
AccountID uint `json:"account_id"`
|
||||
Channel Channel `json:"channel"`
|
||||
Notify NotifyLevel `json:"notify"`
|
||||
PowerLevel int `json:"power_level"`
|
||||
ReadingAnchor *int `json:"reading_anchor"`
|
||||
|
||||
Calls []Call `json:"calls" gorm:"foreignKey:FounderID"`
|
||||
Events []Event `json:"events" gorm:"foreignKey:SenderID"`
|
||||
}
|
39
pkg/internal/models/events.go
Normal file
39
pkg/internal/models/events.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
const (
|
||||
EventMessageNew = "messages.new"
|
||||
EventMessageEdit = "messages.edit"
|
||||
EventMessageDelete = "messages.delete"
|
||||
EventSystemChanges = "system.changes"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
cruda.BaseModel
|
||||
|
||||
Uuid string `json:"uuid"`
|
||||
Body datatypes.JSONMap `json:"body"`
|
||||
Type string `json:"type"`
|
||||
Channel Channel `json:"channel"`
|
||||
Sender ChannelMember `json:"sender"`
|
||||
QuoteEventID *uint `json:"quote_event_id,omitempty"`
|
||||
RelatedEventID *uint `json:"related_event_id,omitempty"`
|
||||
ChannelID uint `json:"channel_id"`
|
||||
SenderID uint `json:"sender_id"`
|
||||
}
|
||||
|
||||
// Event Payloads
|
||||
|
||||
type EventMessageBody struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
KeyPair string `json:"keypair_id,omitempty"`
|
||||
Algorithm string `json:"algorithm,omitempty"`
|
||||
Attachments []string `json:"attachments,omitempty"`
|
||||
QuoteEventID *uint `json:"quote_event,omitempty"`
|
||||
RelatedEventID *uint `json:"related_event,omitempty"`
|
||||
RelatedUsers []uint `json:"related_users,omitempty"`
|
||||
}
|
222
pkg/internal/services/calls.go
Normal file
222
pkg/internal/services/calls.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/livekit/protocol/auth"
|
||||
"github.com/livekit/protocol/livekit"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ListCall(channel models.Channel, take, offset int) ([]models.Call, error) {
|
||||
var calls []models.Call
|
||||
if err := database.C.
|
||||
Where(models.Call{ChannelID: channel.ID}).
|
||||
Limit(take).
|
||||
Offset(offset).
|
||||
Preload("Founder").
|
||||
Preload("Channel").
|
||||
Order("created_at DESC").
|
||||
Find(&calls).Error; err != nil {
|
||||
return calls, err
|
||||
} else {
|
||||
return calls, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetCall(channel models.Channel, id uint) (models.Call, error) {
|
||||
var call models.Call
|
||||
if err := database.C.
|
||||
Where(models.Call{
|
||||
BaseModel: cruda.BaseModel{ID: id},
|
||||
ChannelID: channel.ID,
|
||||
}).
|
||||
Preload("Founder").
|
||||
Preload("Channel").
|
||||
Order("created_at DESC").
|
||||
First(&call).Error; err != nil {
|
||||
return call, err
|
||||
} else {
|
||||
return call, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetOngoingCall(channel models.Channel) (models.Call, error) {
|
||||
var call models.Call
|
||||
if err := database.C.
|
||||
Where(models.Call{ChannelID: channel.ID}).
|
||||
Where("ended_at IS NULL").
|
||||
Preload("Founder").
|
||||
Preload("Channel").
|
||||
Order("created_at DESC").
|
||||
First(&call).Error; err != nil {
|
||||
return call, err
|
||||
} else {
|
||||
return call, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetCallParticipants(call models.Call) ([]*livekit.ParticipantInfo, error) {
|
||||
res, err := Lk.ListParticipants(context.Background(), &livekit.ListParticipantsRequest{
|
||||
Room: call.ExternalID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res.Participants, nil
|
||||
}
|
||||
|
||||
func NewCall(channel models.Channel, founder models.ChannelMember) (models.Call, error) {
|
||||
id := fmt.Sprintf("%s+%d", channel.Alias, channel.ID)
|
||||
call := models.Call{
|
||||
ExternalID: id,
|
||||
FounderID: founder.ID,
|
||||
ChannelID: channel.ID,
|
||||
Founder: founder,
|
||||
Channel: channel,
|
||||
}
|
||||
|
||||
if _, err := GetOngoingCall(channel); err == nil || !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return call, fmt.Errorf("this channel already has an ongoing call")
|
||||
}
|
||||
|
||||
_, err := Lk.CreateRoom(context.Background(), &livekit.CreateRoomRequest{
|
||||
Name: id,
|
||||
EmptyTimeout: viper.GetUint32("calling.empty_timeout_duration"),
|
||||
MaxParticipants: viper.GetUint32("calling.max_participants"),
|
||||
})
|
||||
if err != nil {
|
||||
return call, fmt.Errorf("remote livekit error: %v", err)
|
||||
}
|
||||
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&call).Error; err != nil {
|
||||
return call, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: call.ChannelID,
|
||||
}).Find(&members).Error; err == nil {
|
||||
call, _ = GetCall(call.Channel, call.ID)
|
||||
var pendingUsers []uint64
|
||||
for _, member := range members {
|
||||
if member.ID != call.Founder.ID {
|
||||
pendingUsers = append(pendingUsers, uint64(member.AccountID))
|
||||
}
|
||||
}
|
||||
|
||||
channel, _ = GetChannel(channel.ID)
|
||||
if channel.RealmID != nil {
|
||||
realm, err := authkit.GetRealm(gap.Nx, *channel.RealmID)
|
||||
if err == nil {
|
||||
channel.Realm = &realm
|
||||
}
|
||||
}
|
||||
|
||||
// The call notification is not happen very often
|
||||
// So we don't need to optimize the performance for passive users
|
||||
PushCommandBatch(pendingUsers, nex.WebSocketPackage{
|
||||
Action: "calls.new",
|
||||
Payload: call,
|
||||
})
|
||||
|
||||
err = authkit.NotifyUserBatch(
|
||||
gap.Nx,
|
||||
pendingUsers,
|
||||
pushkit.Notification{
|
||||
Topic: "messaging.callStart",
|
||||
Title: fmt.Sprintf("Call in (%s)", channel.DisplayText()),
|
||||
Body: fmt.Sprintf("%s is calling", call.Founder.Name),
|
||||
Metadata: map[string]any{
|
||||
"avatar": call.Founder.Avatar,
|
||||
"user_id": call.Founder.AccountID,
|
||||
"user_name": call.Founder.Name,
|
||||
"user_nick": call.Founder.Nick,
|
||||
"channel_id": call.ChannelID,
|
||||
},
|
||||
Priority: 5,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("An error occurred when trying notify user.")
|
||||
}
|
||||
}
|
||||
|
||||
return call, nil
|
||||
}
|
||||
|
||||
func EndCall(call models.Call) (models.Call, error) {
|
||||
call.EndedAt = lo.ToPtr(time.Now())
|
||||
|
||||
if _, err := Lk.DeleteRoom(context.Background(), &livekit.DeleteRoomRequest{
|
||||
Room: call.ExternalID,
|
||||
}); err != nil {
|
||||
log.Error().Err(err).Msg("Unable to delete room at livekit side")
|
||||
}
|
||||
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&call).Error; err != nil {
|
||||
return call, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: call.ChannelID,
|
||||
}).Find(&members).Error; err == nil {
|
||||
call, _ = GetCall(call.Channel, call.ID)
|
||||
var pendingUsers []uint64
|
||||
for _, member := range members {
|
||||
if member.ID != call.Founder.ID {
|
||||
pendingUsers = append(pendingUsers, uint64(member.AccountID))
|
||||
}
|
||||
}
|
||||
|
||||
PushCommandBatch(pendingUsers, nex.WebSocketPackage{
|
||||
Action: "calls.end",
|
||||
Payload: call,
|
||||
})
|
||||
}
|
||||
|
||||
return call, nil
|
||||
}
|
||||
|
||||
func KickParticipantInCall(call models.Call, username string) error {
|
||||
_, err := Lk.RemoveParticipant(context.Background(), &livekit.RoomParticipantIdentity{
|
||||
Room: call.ExternalID,
|
||||
Identity: username,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func EncodeCallToken(user authm.Account, call models.Call) (string, error) {
|
||||
isAdmin := user.ID == call.FounderID || user.ID == call.Channel.AccountID
|
||||
|
||||
grant := &auth.VideoGrant{
|
||||
Room: call.ExternalID,
|
||||
RoomJoin: true,
|
||||
RoomAdmin: isAdmin,
|
||||
}
|
||||
|
||||
metadata, _ := jsoniter.Marshal(user)
|
||||
|
||||
duration := time.Second * time.Duration(viper.GetInt("calling.token_duration"))
|
||||
tk := auth.NewAccessToken(viper.GetString("calling.api_key"), viper.GetString("calling.api_secret"))
|
||||
tk.AddGrant(grant).
|
||||
SetIdentity(user.Name).
|
||||
SetName(user.Nick).
|
||||
SetMetadata(string(metadata)).
|
||||
SetValidFor(duration)
|
||||
|
||||
return tk.ToJWT()
|
||||
}
|
118
pkg/internal/services/channel_members.go
Normal file
118
pkg/internal/services/channel_members.go
Normal file
@@ -0,0 +1,118 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
)
|
||||
|
||||
func CountChannelMember(channelId uint) (int64, error) {
|
||||
var count int64
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channelId,
|
||||
}).Model(&models.ChannelMember{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
} else {
|
||||
return count, nil
|
||||
}
|
||||
}
|
||||
|
||||
func ListChannelMember(channelId uint, take int, offset int) ([]models.ChannelMember, error) {
|
||||
var members []models.ChannelMember
|
||||
|
||||
if err := database.C.
|
||||
Limit(take).Offset(offset).
|
||||
Where(&models.ChannelMember{ChannelID: channelId}).
|
||||
Find(&members).Error; err != nil {
|
||||
return members, err
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func GetChannelMember(user authm.Account, channelId uint) (models.ChannelMember, error) {
|
||||
var member models.ChannelMember
|
||||
|
||||
if err := database.C.
|
||||
Where(&models.ChannelMember{AccountID: user.ID, ChannelID: channelId}).
|
||||
First(&member).Error; err != nil {
|
||||
return member, err
|
||||
}
|
||||
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func AddChannelMemberWithCheck(user, op authm.Account, target models.Channel) error {
|
||||
if user.ID != op.ID {
|
||||
if err := authkit.EnsureUserPermGranted(gap.Nx, user.ID, op.ID, "ChannelAdd", true); err != nil {
|
||||
return fmt.Errorf("unable to add user into your channel due to access denied: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return AddChannelMember(user, target)
|
||||
}
|
||||
|
||||
func AddChannelMember(user authm.Account, target models.Channel) error {
|
||||
var member models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
ChannelID: target.ID,
|
||||
}).First(&member).Error; err == nil || !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
member = models.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
err := database.C.Save(&member).Error
|
||||
|
||||
if err == nil {
|
||||
cachekit.DeleteByTags(
|
||||
gap.Ca,
|
||||
fmt.Sprintf("channel#%d", target.ID),
|
||||
fmt.Sprintf("user#%d", user.ID),
|
||||
)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func EditChannelMember(membership models.ChannelMember) (models.ChannelMember, error) {
|
||||
if err := database.C.Save(&membership).Error; err != nil {
|
||||
return membership, err
|
||||
} else {
|
||||
cachekit.DeleteByTags(
|
||||
gap.Ca,
|
||||
fmt.Sprintf("channel#%d", membership.ChannelID),
|
||||
fmt.Sprintf("user#%d", membership.AccountID),
|
||||
)
|
||||
}
|
||||
|
||||
return membership, nil
|
||||
}
|
||||
|
||||
func RemoveChannelMember(member models.ChannelMember, target models.Channel) error {
|
||||
if err := database.C.Delete(&member).Error; err == nil {
|
||||
database.C.Where("sender_id = ?").Delete(&models.Event{})
|
||||
|
||||
cachekit.DeleteByTags(
|
||||
gap.Ca,
|
||||
fmt.Sprintf("channel#%d", target.ID),
|
||||
fmt.Sprintf("user#%d", member.AccountID),
|
||||
)
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
300
pkg/internal/services/channels.go
Normal file
300
pkg/internal/services/channels.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type channelIdentityCacheEntry struct {
|
||||
Channel models.Channel `json:"channel"`
|
||||
ChannelMember models.ChannelMember `json:"channel_member"`
|
||||
}
|
||||
|
||||
func KgChannelIdentityCache(channel string, user uint, realm ...uint) string {
|
||||
if len(realm) > 0 {
|
||||
return fmt.Sprintf("channel-identity-%s#%d@%d", channel, user, realm)
|
||||
} else {
|
||||
return fmt.Sprintf("channel-identity-%s#%d", channel, user)
|
||||
}
|
||||
}
|
||||
|
||||
func CacheChannelIdentity(channel models.Channel, member models.ChannelMember, user uint, realm ...uint) {
|
||||
key := KgChannelIdentityCache(channel.Alias, user, realm...)
|
||||
|
||||
cachekit.Set(
|
||||
gap.Ca,
|
||||
key,
|
||||
channelIdentityCacheEntry{channel, member},
|
||||
60*time.Minute,
|
||||
fmt.Sprintf("channel#%d", channel.ID),
|
||||
fmt.Sprintf("user#%d", user),
|
||||
)
|
||||
}
|
||||
|
||||
func GetChannelIdentityWithID(id uint, user uint) (models.Channel, models.ChannelMember, error) {
|
||||
var member models.ChannelMember
|
||||
|
||||
if err := database.C.Where(models.ChannelMember{
|
||||
AccountID: user,
|
||||
ChannelID: id,
|
||||
}).Preload("Channel").First(&member).Error; err != nil {
|
||||
return member.Channel, member, fmt.Errorf("channel principal not found: %v", err.Error())
|
||||
}
|
||||
|
||||
return member.Channel, member, nil
|
||||
}
|
||||
|
||||
func GetChannelIdentity(alias string, user uint, realm ...authm.Realm) (models.Channel, models.ChannelMember, error) {
|
||||
var err error
|
||||
var channel models.Channel
|
||||
var member models.ChannelMember
|
||||
|
||||
hitCache := false
|
||||
if len(realm) > 0 {
|
||||
if val, err := cachekit.Get[channelIdentityCacheEntry](
|
||||
gap.Ca,
|
||||
KgChannelIdentityCache(alias, user, realm[0].ID),
|
||||
); err == nil {
|
||||
channel = val.Channel
|
||||
member = val.ChannelMember
|
||||
hitCache = true
|
||||
}
|
||||
} else {
|
||||
if val, err := cachekit.Get[channelIdentityCacheEntry](
|
||||
gap.Ca,
|
||||
KgChannelIdentityCache(alias, user),
|
||||
); err == nil {
|
||||
channel = val.Channel
|
||||
member = val.ChannelMember
|
||||
hitCache = true
|
||||
}
|
||||
}
|
||||
if !hitCache {
|
||||
if len(realm) > 0 {
|
||||
channel, member, err = GetAvailableChannelWithAlias(alias, user, realm[0].ID)
|
||||
CacheChannelIdentity(channel, member, user, realm[0].ID)
|
||||
} else {
|
||||
channel, member, err = GetAvailableChannelWithAlias(alias, user)
|
||||
CacheChannelIdentity(channel, member, user)
|
||||
}
|
||||
}
|
||||
|
||||
return channel, member, err
|
||||
}
|
||||
|
||||
func GetChannelAliasAvailability(alias string) error {
|
||||
if !regexp.MustCompile("^[a-z0-9-]+$").MatchString(alias) {
|
||||
return fmt.Errorf("channel alias should only contains lowercase letters, numbers, and hyphens")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetChannel(id uint) (models.Channel, error) {
|
||||
var channel models.Channel
|
||||
tx := database.C.Where("id = ?", id)
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
if err := tx.First(&channel).Error; err != nil {
|
||||
return channel, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func GetChannelWithAlias(alias string, realmId ...uint) (models.Channel, error) {
|
||||
var channel models.Channel
|
||||
tx := database.C.Where(models.Channel{Alias: alias})
|
||||
if len(realmId) > 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
} else {
|
||||
tx = tx.Where("realm_id IS NULL")
|
||||
}
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
if err := tx.First(&channel).Error; err != nil {
|
||||
return channel, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func GetAvailableChannelWithAlias(alias string, user uint, realmId ...uint) (models.Channel, models.ChannelMember, error) {
|
||||
var err error
|
||||
var member models.ChannelMember
|
||||
var channel models.Channel
|
||||
if channel, err = GetChannelWithAlias(alias, realmId...); err != nil {
|
||||
return channel, member, err
|
||||
}
|
||||
|
||||
if err := database.C.Where(models.ChannelMember{
|
||||
AccountID: user,
|
||||
ChannelID: channel.ID,
|
||||
}).First(&member).Error; err != nil {
|
||||
return channel, member, fmt.Errorf("channel principal not found: %v", err.Error())
|
||||
}
|
||||
|
||||
return channel, member, nil
|
||||
}
|
||||
|
||||
func GetAvailableChannel(id uint, user authm.Account) (models.Channel, models.ChannelMember, error) {
|
||||
var err error
|
||||
var member models.ChannelMember
|
||||
var channel models.Channel
|
||||
if channel, err = GetChannel(id); err != nil {
|
||||
return channel, member, err
|
||||
}
|
||||
tx := database.C.Where(models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
ChannelID: channel.ID,
|
||||
})
|
||||
if err := tx.First(&member).Error; err != nil {
|
||||
return channel, member, fmt.Errorf("channel principal not found: %v", err.Error())
|
||||
}
|
||||
|
||||
return channel, member, nil
|
||||
}
|
||||
|
||||
func PreloadDirectChannelMembers(tx *gorm.DB) *gorm.DB {
|
||||
return tx.Preload("Members", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Joins(
|
||||
fmt.Sprintf(
|
||||
"JOIN %schannels AS c ON c.type = ?",
|
||||
viper.GetString("database.prefix"),
|
||||
),
|
||||
models.ChannelTypeDirect,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func ListChannel(user *authm.Account, realmId ...uint) ([]models.Channel, error) {
|
||||
var identities []models.ChannelMember
|
||||
var idRange []uint
|
||||
if user != nil {
|
||||
if err := database.C.Where("account_id = ?", user.ID).Find(&identities).Error; err != nil {
|
||||
return nil, fmt.Errorf("unable to get identities: %v", err)
|
||||
}
|
||||
for _, identity := range identities {
|
||||
idRange = append(idRange, identity.ChannelID)
|
||||
}
|
||||
}
|
||||
|
||||
var channels []models.Channel
|
||||
tx := database.C
|
||||
tx = tx.Where("id IN ? OR is_public = true", idRange)
|
||||
if len(realmId) > 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
}
|
||||
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
|
||||
if err := tx.Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func ListChannelPublic(realmId ...uint) ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
tx := database.C
|
||||
tx = tx.Where("is_public = true")
|
||||
if len(realmId) > 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
}
|
||||
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
|
||||
if err := tx.Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func ListChannelWithUser(user authm.Account, realmId ...uint) ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
tx := database.C.Where(&models.Channel{AccountID: user.ID})
|
||||
if len(realmId) > 0 {
|
||||
if realmId[0] != 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
}
|
||||
}
|
||||
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
|
||||
if err := tx.Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func ListAvailableChannel(tx *gorm.DB, user authm.Account, realmId ...uint) ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
}).Find(&members).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
idx := lo.Map(members, func(item models.ChannelMember, index int) uint {
|
||||
return item.ChannelID
|
||||
})
|
||||
|
||||
tx = tx.Where("id IN ?", idx)
|
||||
if len(realmId) > 0 {
|
||||
if realmId[0] != 0 {
|
||||
tx = tx.Where("realm_id = ?", realmId)
|
||||
}
|
||||
} else {
|
||||
tx = tx.Where("realm_id IS NULL")
|
||||
}
|
||||
|
||||
tx = PreloadDirectChannelMembers(tx)
|
||||
|
||||
if err := tx.Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func NewChannel(channel models.Channel) (models.Channel, error) {
|
||||
err := database.C.Save(&channel).Error
|
||||
return channel, err
|
||||
}
|
||||
|
||||
func EditChannel(channel models.Channel) (models.Channel, error) {
|
||||
err := database.C.Save(&channel).Error
|
||||
|
||||
if err == nil {
|
||||
cachekit.DeleteByTags(gap.Ca, fmt.Sprintf("channel#%d", channel.ID))
|
||||
}
|
||||
|
||||
return channel, err
|
||||
}
|
||||
|
||||
func DeleteChannel(channel models.Channel) error {
|
||||
if err := database.C.Delete(&channel).Error; err == nil {
|
||||
UnsubscribeAllWithChannels(channel.ID)
|
||||
|
||||
database.C.Where("channel_id = ?", channel.ID).Delete(&models.Event{})
|
||||
database.C.Where("channel_id = ?", channel.ID).Delete(&models.ChannelMember{})
|
||||
|
||||
cachekit.DeleteByTags(gap.Ca, fmt.Sprintf("channel#%d", channel.ID))
|
||||
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
@@ -1,10 +1,10 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func DoAutoDatabaseCleanup() {
|
||||
@@ -13,21 +13,13 @@ func DoAutoDatabaseCleanup() {
|
||||
|
||||
// Deal soft-deletion
|
||||
var count int64
|
||||
for _, model := range database.DatabaseAutoActionRange {
|
||||
for _, model := range database.AutoMaintainRange {
|
||||
tx := database.C.Unscoped().Delete(model, "deleted_at >= ?", deadline)
|
||||
if tx.Error != nil {
|
||||
log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...")
|
||||
log.Error().Err(tx.Error).Msg("An error occurred when running database cleanup...")
|
||||
}
|
||||
count += tx.RowsAffected
|
||||
}
|
||||
|
||||
// Clean up outdated chat history
|
||||
tx := database.C.Unscoped().Delete(&models.Message{}, "created_at < ?", time.Now().Add(30*24*time.Hour))
|
||||
if tx.Error != nil {
|
||||
log.Error().Err(tx.Error).Msg("An error occurred when running auth context cleanup...")
|
||||
} else {
|
||||
count += tx.RowsAffected
|
||||
}
|
||||
|
||||
log.Debug().Int64("affected", count).Msg("Clean up entire database accomplished.")
|
||||
}
|
25
pkg/internal/services/direct_channels.go
Normal file
25
pkg/internal/services/direct_channels.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
)
|
||||
|
||||
func GetDirectChannelByUser(user authm.Account, other authm.Account) (models.Channel, error) {
|
||||
memberTable := "channel_members"
|
||||
channelTable := "channels"
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Preload("Members").
|
||||
Where("type = ?", models.ChannelTypeDirect).
|
||||
Joins(fmt.Sprintf("JOIN %s cm1 ON cm1.channel_id = %s.id AND cm1.account_id = ?", memberTable, channelTable), user.ID).
|
||||
Joins(fmt.Sprintf("JOIN %s cm2 ON cm2.channel_id = %s.id AND cm2.account_id = ?", memberTable, channelTable), other.ID).
|
||||
First(&channel).Error; err != nil {
|
||||
return channel, err
|
||||
} else {
|
||||
return channel, nil
|
||||
}
|
||||
}
|
297
pkg/internal/services/events.go
Normal file
297
pkg/internal/services/events.go
Normal file
@@ -0,0 +1,297 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
||||
"git.solsynth.dev/hypernet/paperclip/pkg/filekit"
|
||||
"git.solsynth.dev/hypernet/paperclip/pkg/proto"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||
"git.solsynth.dev/hypernet/pusher/pkg/pushkit"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func CountEvent(channel models.Channel) int64 {
|
||||
var count int64
|
||||
if err := database.C.Where(models.Event{
|
||||
ChannelID: channel.ID,
|
||||
}).Model(&models.Event{}).Count(&count).Error; err != nil {
|
||||
return 0
|
||||
} else {
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
func ListEvent(channel models.Channel, take int, offset int) ([]models.Event, error) {
|
||||
if take > 100 {
|
||||
take = 100
|
||||
}
|
||||
|
||||
var events []models.Event
|
||||
if err := database.C.
|
||||
Where(models.Event{
|
||||
ChannelID: channel.ID,
|
||||
}).Limit(take).Offset(offset).
|
||||
Order("created_at DESC").
|
||||
Preload("Sender").
|
||||
Find(&events).Error; err != nil {
|
||||
return events, err
|
||||
} else {
|
||||
return events, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetEvent(channelId uint, id uint) (models.Event, error) {
|
||||
var event models.Event
|
||||
if err := database.C.
|
||||
Where("id = ? AND channel_id = ?", id, channelId).
|
||||
Preload("Sender").
|
||||
First(&event).Error; err != nil {
|
||||
return event, err
|
||||
} else {
|
||||
return event, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetEventWithSender(channel models.Channel, member models.ChannelMember, id uint) (models.Event, error) {
|
||||
var event models.Event
|
||||
if err := database.C.Where(models.Event{
|
||||
BaseModel: cruda.BaseModel{ID: id},
|
||||
ChannelID: channel.ID,
|
||||
SenderID: member.ID,
|
||||
}).First(&event).Error; err != nil {
|
||||
return event, err
|
||||
} else {
|
||||
return event, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewEvent(event models.Event) (models.Event, error) {
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&event).Error; err != nil {
|
||||
return event, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: event.ChannelID,
|
||||
}).Find(&members).Error; err != nil {
|
||||
// Couldn't get channel members, skip notifying
|
||||
log.Warn().Err(err).Msg("Failed to fetch members, the notifying of new event was terminated...")
|
||||
return event, nil
|
||||
}
|
||||
|
||||
if val, ok := event.Body["attachments"].([]string); ok && len(val) > 0 {
|
||||
filekit.CountAttachmentUsage(gap.Nx, &proto.UpdateUsageRequest{
|
||||
Rid: val,
|
||||
Delta: 1,
|
||||
})
|
||||
}
|
||||
|
||||
event, err := GetEvent(event.ChannelID, event.ID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to fetch event, the notifying of new event was terminated...")
|
||||
return event, err
|
||||
}
|
||||
idxList := lo.Map(lo.Filter(members, func(item models.ChannelMember, index int) bool {
|
||||
if !viper.GetBool("performance.passive_user_optimize") {
|
||||
// Leave this for backward compatibility
|
||||
return true
|
||||
}
|
||||
return CheckSubscribed(item.AccountID, event.ChannelID)
|
||||
}), func(item models.ChannelMember, index int) uint64 {
|
||||
return uint64(item.AccountID)
|
||||
})
|
||||
_ = PushCommandBatch(idxList, nex.WebSocketPackage{
|
||||
Action: "events.new",
|
||||
Payload: event,
|
||||
})
|
||||
|
||||
if strings.HasPrefix(event.Type, "messages") {
|
||||
event.Channel, _ = GetChannel(event.ChannelID)
|
||||
if event.Channel.RealmID != nil {
|
||||
realm, err := authkit.GetRealm(gap.Nx, *event.Channel.RealmID)
|
||||
if err == nil {
|
||||
event.Channel.Realm = &realm
|
||||
}
|
||||
}
|
||||
go NotifyMessageEvent(members, event)
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func NotifyMessageEvent(members []models.ChannelMember, event models.Event) {
|
||||
var body models.EventMessageBody
|
||||
raw, _ := jsoniter.Marshal(event.Body)
|
||||
_ = jsoniter.Unmarshal(raw, &body)
|
||||
|
||||
var pendingUsers []uint64
|
||||
var mentionedUsers []uint64
|
||||
|
||||
for _, member := range members {
|
||||
if CheckSubscribed(member.AccountID, event.ChannelID) {
|
||||
continue
|
||||
}
|
||||
if member.ID != event.SenderID {
|
||||
switch member.Notify {
|
||||
case models.NotifyLevelNone:
|
||||
continue
|
||||
case models.NotifyLevelMentioned:
|
||||
if len(body.RelatedUsers) != 0 && lo.Contains(body.RelatedUsers, member.AccountID) {
|
||||
mentionedUsers = append(mentionedUsers, uint64(member.AccountID))
|
||||
}
|
||||
continue
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if lo.Contains(body.RelatedUsers, member.AccountID) {
|
||||
mentionedUsers = append(mentionedUsers, uint64(member.AccountID))
|
||||
} else {
|
||||
pendingUsers = append(pendingUsers, uint64(member.AccountID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var displayText string
|
||||
var displaySubtitle string
|
||||
switch event.Type {
|
||||
case models.EventMessageNew:
|
||||
if body.Algorithm == "plain" {
|
||||
displayText = body.Text
|
||||
}
|
||||
case models.EventMessageEdit:
|
||||
displaySubtitle = "Edited a message"
|
||||
if body.Algorithm == "plain" {
|
||||
displayText = body.Text
|
||||
}
|
||||
case models.EventMessageDelete:
|
||||
displayText = "Deleted a message"
|
||||
}
|
||||
|
||||
if len(displayText) == 0 {
|
||||
if len(displayText) == 1 {
|
||||
displayText = fmt.Sprintf("%d file", len(body.Attachments))
|
||||
} else {
|
||||
displayText = fmt.Sprintf("%d files", len(body.Attachments))
|
||||
}
|
||||
} else if len(body.Attachments) > 0 {
|
||||
if len(displayText) == 1 {
|
||||
displayText += fmt.Sprintf(" (%d file)", len(body.Attachments))
|
||||
} else {
|
||||
displayText += fmt.Sprintf(" (%d files)", len(body.Attachments))
|
||||
}
|
||||
}
|
||||
|
||||
user, err := authkit.GetUser(gap.Nx, event.Sender.AccountID)
|
||||
if err == nil {
|
||||
event.Sender.Avatar = user.Avatar
|
||||
if len(event.Sender.Nick) == 0 {
|
||||
event.Sender.Nick = user.Nick
|
||||
}
|
||||
}
|
||||
|
||||
displayTitle := fmt.Sprintf("%s (%s)", event.Sender.Nick, event.Channel.DisplayText())
|
||||
|
||||
metadata := map[string]any{
|
||||
"avatar": event.Sender.Avatar,
|
||||
"user_id": event.Sender.AccountID,
|
||||
"user_name": event.Sender.Name,
|
||||
"user_nick": event.Sender.Nick,
|
||||
"channel_id": event.ChannelID,
|
||||
"event_id": event.ID,
|
||||
}
|
||||
|
||||
if len(pendingUsers) > 0 {
|
||||
log.Debug().
|
||||
Uint("event_id", event.ID).
|
||||
Str("title", displayTitle).
|
||||
Int("count", len(pendingUsers)).
|
||||
Msg("Notifying new event...")
|
||||
|
||||
for _, pendingUser := range pendingUsers {
|
||||
replyToken, err := CreateReplyToken(event.ID, uint(pendingUser))
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("An error occurred when trying create reply token.")
|
||||
continue
|
||||
}
|
||||
metadata["reply_token"] = replyToken
|
||||
err = authkit.NotifyUser(
|
||||
gap.Nx,
|
||||
pendingUser,
|
||||
pushkit.Notification{
|
||||
Topic: "messaging.message",
|
||||
Title: displayTitle,
|
||||
Subtitle: displaySubtitle,
|
||||
Body: displayText,
|
||||
Metadata: metadata,
|
||||
Priority: 10,
|
||||
},
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("An error occurred when trying notify user.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(mentionedUsers) > 0 {
|
||||
if len(displaySubtitle) > 0 {
|
||||
displaySubtitle += ", and mentioned you"
|
||||
} else {
|
||||
displaySubtitle = "Mentioned you"
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Uint("event_id", event.ID).
|
||||
Str("title", displayTitle).
|
||||
Int("count", len(mentionedUsers)).
|
||||
Msg("Notifying new event...")
|
||||
|
||||
for _, mentionedUser := range mentionedUsers {
|
||||
replyToken, err := CreateReplyToken(event.ID, uint(mentionedUser))
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("An error occurred when trying create reply token.")
|
||||
continue
|
||||
}
|
||||
metadata["reply_token"] = replyToken
|
||||
err = authkit.NotifyUser(
|
||||
gap.Nx,
|
||||
mentionedUser,
|
||||
pushkit.Notification{
|
||||
Topic: "messaging.message",
|
||||
Title: displayTitle,
|
||||
Subtitle: displaySubtitle,
|
||||
Body: displayText,
|
||||
Metadata: metadata,
|
||||
Priority: 10,
|
||||
},
|
||||
true,
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("An error occurred when trying notify user.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EditEvent(event models.Event) (models.Event, error) {
|
||||
if err := database.C.Save(&event).Error; err != nil {
|
||||
return event, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func DeleteEvent(event models.Event) (models.Event, error) {
|
||||
if err := database.C.Delete(&event).Error; err != nil {
|
||||
return event, err
|
||||
}
|
||||
return event, nil
|
||||
}
|
18
pkg/internal/services/livekit.go
Normal file
18
pkg/internal/services/livekit.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
lksdk "github.com/livekit/server-sdk-go"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var Lk *lksdk.RoomServiceClient
|
||||
|
||||
func SetupLiveKit() {
|
||||
host := "https://" + viper.GetString("calling.endpoint")
|
||||
|
||||
Lk = lksdk.NewRoomServiceClient(
|
||||
host,
|
||||
viper.GetString("calling.api_key"),
|
||||
viper.GetString("calling.api_secret"),
|
||||
)
|
||||
}
|
64
pkg/internal/services/messages.go
Normal file
64
pkg/internal/services/messages.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"github.com/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func EncodeMessageBody(body models.EventMessageBody) map[string]any {
|
||||
var parsed map[string]any
|
||||
raw, _ := jsoniter.Marshal(body)
|
||||
_ = jsoniter.Unmarshal(raw, &parsed)
|
||||
return parsed
|
||||
}
|
||||
|
||||
func EditMessage(event models.Event, body models.EventMessageBody) (models.Event, error) {
|
||||
event.Body = EncodeMessageBody(body)
|
||||
event, err := EditEvent(event)
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
body.RelatedEventID = &event.ID
|
||||
_, err = NewEvent(models.Event{
|
||||
Uuid: uuid.NewString(),
|
||||
Body: EncodeMessageBody(body),
|
||||
Type: models.EventMessageEdit,
|
||||
Channel: event.Channel,
|
||||
Sender: event.Sender,
|
||||
QuoteEventID: body.QuoteEventID,
|
||||
RelatedEventID: &event.ID,
|
||||
ChannelID: event.ChannelID,
|
||||
SenderID: event.SenderID,
|
||||
})
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
||||
|
||||
func DeleteMessage(event models.Event) (models.Event, error) {
|
||||
clonedEvent := event
|
||||
_, err := DeleteEvent(clonedEvent)
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
_, err = NewEvent(models.Event{
|
||||
Uuid: uuid.NewString(),
|
||||
Body: EncodeMessageBody(models.EventMessageBody{
|
||||
RelatedEventID: &event.ID,
|
||||
}),
|
||||
Type: models.EventMessageDelete,
|
||||
Channel: event.Channel,
|
||||
Sender: event.Sender,
|
||||
RelatedEventID: &event.ID,
|
||||
ChannelID: event.ChannelID,
|
||||
SenderID: event.SenderID,
|
||||
})
|
||||
if err != nil {
|
||||
return event, err
|
||||
}
|
||||
|
||||
return event, nil
|
||||
}
|
35
pkg/internal/services/reading_anchor.go
Normal file
35
pkg/internal/services/reading_anchor.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var readingAnchorQueue = make(map[uint]uint)
|
||||
|
||||
func SetReadingAnchor(memberId uint, eventId uint) {
|
||||
if val, ok := readingAnchorQueue[memberId]; ok {
|
||||
readingAnchorQueue[memberId] = max(eventId, val)
|
||||
} else {
|
||||
readingAnchorQueue[memberId] = eventId
|
||||
}
|
||||
}
|
||||
|
||||
func FlushReadingAnchor() {
|
||||
if len(readingAnchorQueue) == 0 {
|
||||
return
|
||||
}
|
||||
for k, v := range readingAnchorQueue {
|
||||
if err := database.C.Model(&models.ChannelMember{}).
|
||||
Where("id = ?", k).
|
||||
Updates(map[string]any{
|
||||
"reading_anchor": gorm.Expr("GREATEST(COALESCE(reading_anchor, 0), ?)", v),
|
||||
}).Error; err != nil {
|
||||
log.Error().Err(err).Msg("An error occurred when flushing reading anchor...")
|
||||
return
|
||||
}
|
||||
}
|
||||
clear(readingAnchorQueue)
|
||||
}
|
58
pkg/internal/services/reply_token.go
Normal file
58
pkg/internal/services/reply_token.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type ReplyClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
EventID uint `json:"event_id"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func CreateReplyToken(eventId uint, userId uint) (string, error) {
|
||||
claims := ReplyClaims{
|
||||
UserID: userId,
|
||||
EventID: eventId,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Issuer: "messaging",
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24 * 30)),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS512, claims)
|
||||
tks, err := token.SignedString([]byte(viper.GetString("security.reply_token_secret")))
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign token: %v", err)
|
||||
}
|
||||
return tks, nil
|
||||
}
|
||||
|
||||
func ParseReplyToken(tk string) (ReplyClaims, error) {
|
||||
var claims ReplyClaims
|
||||
token, err := jwt.ParseWithClaims(tk, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Method)
|
||||
}
|
||||
return []byte(viper.GetString("security.reply_token_secret")), nil
|
||||
})
|
||||
if err != nil {
|
||||
// Check if the error is an expired token error
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
// Treat expired token as valid (allow it)
|
||||
return claims, nil
|
||||
}
|
||||
return claims, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return claims, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
91
pkg/internal/services/status.go
Normal file
91
pkg/internal/services/status.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cachekit"
|
||||
"github.com/samber/lo"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type statusQueryCacheEntry struct {
|
||||
Target []uint64
|
||||
Data any
|
||||
}
|
||||
|
||||
func KgTypingStatusCache(channelId uint, userId uint) string {
|
||||
return fmt.Sprintf("chat-typing-status#%d@%d", userId, channelId)
|
||||
}
|
||||
|
||||
func SetTypingStatus(channelId uint, userId uint) error {
|
||||
var broadcastTarget []uint64
|
||||
var data any
|
||||
|
||||
hitCache := false
|
||||
if val, err := cachekit.Get[statusQueryCacheEntry](
|
||||
gap.Ca,
|
||||
KgTypingStatusCache(channelId, userId),
|
||||
); err == nil {
|
||||
broadcastTarget = val.Target
|
||||
data = val.Data
|
||||
hitCache = true
|
||||
}
|
||||
|
||||
if !hitCache {
|
||||
var member models.ChannelMember
|
||||
if err := database.C.
|
||||
Where("account_id = ? AND channel_id = ?", userId, channelId).
|
||||
First(&member).Error; err != nil {
|
||||
return fmt.Errorf("channel member not found: %v", err)
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.
|
||||
Preload("Members").
|
||||
Where("id = ?", channelId).
|
||||
First(&channel).Error; err != nil {
|
||||
return fmt.Errorf("channel not found: %v", err)
|
||||
}
|
||||
|
||||
for _, item := range channel.Members {
|
||||
broadcastTarget = append(broadcastTarget, uint64(item.AccountID))
|
||||
}
|
||||
|
||||
data = map[string]any{
|
||||
"user_id": userId,
|
||||
"member_id": member.ID,
|
||||
"channel_id": channelId,
|
||||
"member": member,
|
||||
"channel": channel,
|
||||
}
|
||||
|
||||
// Cache queries
|
||||
cachekit.Set(
|
||||
gap.Ca,
|
||||
KgTypingStatusCache(channelId, userId),
|
||||
statusQueryCacheEntry{broadcastTarget, data},
|
||||
60*time.Minute,
|
||||
fmt.Sprintf("channel#%d", channelId),
|
||||
)
|
||||
}
|
||||
|
||||
broadcastTarget = lo.Filter(broadcastTarget, func(item uint64, index int) bool {
|
||||
if !viper.GetBool("performance.passive_user_optimize") {
|
||||
// Leave this for backward compatibility
|
||||
return true
|
||||
}
|
||||
return CheckSubscribed(uint(item), channelId)
|
||||
})
|
||||
|
||||
PushCommandBatch(broadcastTarget, nex.WebSocketPackage{
|
||||
Action: "status.typing",
|
||||
Payload: data,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
63
pkg/internal/services/subscribe.go
Normal file
63
pkg/internal/services/subscribe.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package services
|
||||
|
||||
import "sync"
|
||||
|
||||
// ChannelID -> UserID -> Client ID
|
||||
var subscribeInfo = make(map[uint]map[uint]string)
|
||||
var subscribeLock sync.Mutex
|
||||
|
||||
// If user subscribed to a channel
|
||||
// Push the new message to them via websocket
|
||||
// And skip the notification
|
||||
|
||||
func CheckSubscribed(UserID uint, ChannelID uint) bool {
|
||||
if _, ok := subscribeInfo[ChannelID]; ok {
|
||||
if _, ok := subscribeInfo[ChannelID][UserID]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func SubscribeChannel(userId uint, channelId uint, clientId string) {
|
||||
subscribeLock.Lock()
|
||||
defer subscribeLock.Unlock()
|
||||
if _, ok := subscribeInfo[channelId]; !ok {
|
||||
subscribeInfo[channelId] = make(map[uint]string)
|
||||
}
|
||||
subscribeInfo[channelId][userId] = clientId
|
||||
}
|
||||
|
||||
func UnsubscribeChannel(userId uint, channelId uint) {
|
||||
subscribeLock.Lock()
|
||||
defer subscribeLock.Unlock()
|
||||
if _, ok := subscribeInfo[channelId]; ok {
|
||||
delete(subscribeInfo[channelId], userId)
|
||||
}
|
||||
}
|
||||
|
||||
func UnsubscribeAll(userId uint) {
|
||||
subscribeLock.Lock()
|
||||
defer subscribeLock.Unlock()
|
||||
for _, v := range subscribeInfo {
|
||||
delete(v, userId)
|
||||
}
|
||||
}
|
||||
|
||||
func UnsubscribeAllWithChannels(channelId uint) {
|
||||
subscribeLock.Lock()
|
||||
defer subscribeLock.Unlock()
|
||||
delete(subscribeInfo, channelId)
|
||||
}
|
||||
|
||||
func UnsubscribeAllWithClient(clientId string) {
|
||||
subscribeLock.Lock()
|
||||
defer subscribeLock.Unlock()
|
||||
for _, v := range subscribeInfo {
|
||||
for k, item := range v {
|
||||
if item == clientId {
|
||||
delete(v, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
48
pkg/internal/services/websocket.go
Normal file
48
pkg/internal/services/websocket.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/proto"
|
||||
)
|
||||
|
||||
func PushCommand(userId uint, task nex.WebSocketPackage) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pc := gap.Nx.GetNexusGrpcConn()
|
||||
_, err := proto.NewStreamServiceClient(pc).PushStream(ctx, &proto.PushStreamRequest{
|
||||
UserId: lo.ToPtr(uint64(userId)),
|
||||
Body: task.Marshal(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to push websocket command to nexus...")
|
||||
}
|
||||
}
|
||||
|
||||
func PushCommandBatch(userId []uint64, task nex.WebSocketPackage) []uint64 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
pc := gap.Nx.GetNexusGrpcConn()
|
||||
resp, err := proto.NewStreamServiceClient(pc).PushStreamBatch(ctx, &proto.PushStreamBatchRequest{
|
||||
UserId: userId,
|
||||
Body: task.Marshal(),
|
||||
})
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to push websocket command to nexus in batches...")
|
||||
}
|
||||
|
||||
return lo.Map(resp.GetSuccessList(), func(item string, _ int) uint64 {
|
||||
val, _ := strconv.ParseUint(item, 10, 64)
|
||||
return val
|
||||
})
|
||||
}
|
232
pkg/internal/web/api/calls_api.go
Normal file
232
pkg/internal/web/api/calls_api.go
Normal file
@@ -0,0 +1,232 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"sync"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var callLocks sync.Map
|
||||
|
||||
func listCall(c *fiber.Ctx) error {
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if calls, err := services.ListCall(channel, take, offset); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(calls)
|
||||
}
|
||||
}
|
||||
|
||||
func getOngoingCall(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if call, err := services.GetOngoingCall(channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if res, err := services.GetCallParticipants(call); err != nil {
|
||||
return c.JSON(call)
|
||||
} else {
|
||||
call.Participants = res
|
||||
return c.JSON(call)
|
||||
}
|
||||
}
|
||||
|
||||
func startCall(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureGrantedPerm(c, "CreateCalls", true); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).Find(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if membership.PowerLevel < 0 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you have not enough permission to create a call")
|
||||
}
|
||||
|
||||
if _, ok := callLocks.Load(channel.ID); ok {
|
||||
return fiber.NewError(fiber.StatusLocked, "there is already a call in creation progress for this channel")
|
||||
} else {
|
||||
callLocks.Store(channel.ID, true)
|
||||
}
|
||||
|
||||
call, err := services.NewCall(channel, membership)
|
||||
if err != nil {
|
||||
callLocks.Delete(channel.ID)
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
_, _ = services.NewEvent(models.Event{
|
||||
Uuid: uuid.NewString(),
|
||||
Body: map[string]any{},
|
||||
Type: "calls.start",
|
||||
Channel: channel,
|
||||
Sender: membership,
|
||||
ChannelID: channel.ID,
|
||||
SenderID: membership.ID,
|
||||
})
|
||||
|
||||
callLocks.Delete(channel.ID)
|
||||
return c.JSON(call)
|
||||
}
|
||||
}
|
||||
|
||||
func endCall(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).Find(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
call, err := services.GetOngoingCall(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if call.FounderID != membership.ID && membership.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "only call founder or channel moderator can end this call")
|
||||
}
|
||||
|
||||
if call, err := services.EndCall(call); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
_, _ = services.NewEvent(models.Event{
|
||||
Uuid: uuid.NewString(),
|
||||
Body: map[string]any{"last": call.EndedAt.Unix() - call.CreatedAt.Unix()},
|
||||
Type: "calls.end",
|
||||
Channel: channel,
|
||||
Sender: membership,
|
||||
ChannelID: channel.ID,
|
||||
SenderID: membership.ID,
|
||||
})
|
||||
|
||||
return c.JSON(call)
|
||||
}
|
||||
}
|
||||
|
||||
func kickParticipantInCall(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).Find(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
call, err := services.GetOngoingCall(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if call.FounderID != user.ID && membership.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "only call founder or channel admin can kick participant in this call")
|
||||
}
|
||||
|
||||
if err = services.KickParticipantInCall(call, data.Username); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
|
||||
func exchangeCallToken(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).Find(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
call, err := services.GetOngoingCall(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
tk, err := services.EncodeCallToken(user, call)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.JSON(fiber.Map{
|
||||
"token": tk,
|
||||
"endpoint": viper.GetString("calling.endpoint"),
|
||||
})
|
||||
}
|
||||
}
|
267
pkg/internal/web/api/channel_members_api.go
Normal file
267
pkg/internal/web/api/channel_members_api.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/cruda"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"strconv"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listChannelMembers(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
count, err := services.CountChannelMember(channel.ID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
if members, err := services.ListChannelMember(channel.ID, take, offset); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": members,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func addChannelMember(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Related string `json:"related" validate:"required"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if channel.Type == models.ChannelTypeDirect {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "direct message member changes was not allowed")
|
||||
}
|
||||
|
||||
if !channel.IsPublic {
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, err.Error())
|
||||
} else if member.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of a channel to add member into it")
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
var account authm.Account
|
||||
var numericId int
|
||||
if numericId, err = strconv.Atoi(data.Related); err == nil {
|
||||
account, err = authkit.GetUser(gap.Nx, uint(numericId))
|
||||
} else {
|
||||
account, err = authkit.GetUserByName(gap.Nx, data.Related)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.AddChannelMemberWithCheck(account, user, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func removeChannelMember(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
memberId := c.Params("memberId")
|
||||
|
||||
numericId, err := strconv.Atoi(memberId)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "invalid member id")
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if channel.Type == models.ChannelTypeDirect {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "direct message member changes was not allowed")
|
||||
} else if channel.AccountID == user.ID {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "you cannot remove yourself from your own channel")
|
||||
}
|
||||
|
||||
var member models.ChannelMember
|
||||
if me, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, err.Error())
|
||||
} else if me.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of a channel to remove member from it")
|
||||
}
|
||||
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
BaseModel: cruda.BaseModel{ID: uint(numericId)},
|
||||
ChannelID: channel.ID,
|
||||
}).First(&member).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.RemoveChannelMember(member, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func deleteChannelIdentity(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).First(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err = services.RemoveChannelMember(membership, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(membership)
|
||||
}
|
||||
}
|
||||
|
||||
func editChannelIdentity(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Nick string `json:"nick"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).First(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
membership.Name = user.Name
|
||||
if len(data.Nick) > 0 {
|
||||
membership.Nick = data.Nick
|
||||
} else {
|
||||
membership.Nick = user.Nick
|
||||
}
|
||||
|
||||
if membership, err := services.EditChannelMember(membership); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(membership)
|
||||
}
|
||||
}
|
||||
|
||||
func editChannelNotifyLevel(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
NotifyLevel int8 `json:"notify_level"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var membership models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: channel.ID,
|
||||
AccountID: user.ID,
|
||||
}).First(&membership).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
membership.Notify = data.NotifyLevel
|
||||
|
||||
if membership, err := services.EditChannelMember(membership); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.JSON(membership)
|
||||
}
|
||||
}
|
348
pkg/internal/web/api/channels_api.go
Normal file
348
pkg/internal/web/api/channels_api.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func getChannel(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func getChannelIdentity(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else {
|
||||
return c.JSON(member)
|
||||
}
|
||||
}
|
||||
|
||||
func listChannel(c *fiber.Ctx) error {
|
||||
var user *authm.Account
|
||||
if err := sec.EnsureAuthenticated(c); err == nil {
|
||||
user = lo.ToPtr(c.Locals("user").(authm.Account))
|
||||
}
|
||||
|
||||
var err error
|
||||
var channels []models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channels, err = services.ListChannel(user, val.ID)
|
||||
} else {
|
||||
channels, err = services.ListChannel(user)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listPublicChannel(c *fiber.Ctx) error {
|
||||
var err error
|
||||
var channels []models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channels, err = services.ListChannelPublic(val.ID)
|
||||
} else {
|
||||
channels, err = services.ListChannelPublic()
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listOwnedChannel(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
|
||||
var err error
|
||||
var channels []models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channels, err = services.ListChannelWithUser(user, val.ID)
|
||||
} else {
|
||||
channels, err = services.ListChannelWithUser(user)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listOwnedChannelGlobalWide(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
|
||||
channels, err := services.ListChannelWithUser(user, 0)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listAvailableChannel(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
|
||||
tx := database.C
|
||||
isDirect := c.QueryBool("direct", false)
|
||||
if isDirect {
|
||||
tx = tx.Where("type = ?", models.ChannelTypeDirect)
|
||||
} else {
|
||||
tx = tx.Where("type = ?", models.ChannelTypeCommon)
|
||||
}
|
||||
|
||||
var err error
|
||||
var channels []models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channels, err = services.ListAvailableChannel(tx, user, val.ID)
|
||||
} else {
|
||||
channels, err = services.ListAvailableChannel(tx, user)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listAvailableChannelGlobalWide(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
|
||||
channels, err := services.ListAvailableChannel(database.C, user, 0)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func createChannel(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureGrantedPerm(c, "CreateChannels", true); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,lowercase,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsCommunity bool `json:"is_community"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else if err = services.GetChannelAliasAvailability(data.Alias); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
var realm *authm.Realm
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
if info, err := authkit.GetRealmMember(gap.Nx, val.ID, user.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of that realm then can create channel related to it")
|
||||
} else if info.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can create channel related to it")
|
||||
} else {
|
||||
realm = &val
|
||||
}
|
||||
}
|
||||
|
||||
channel := models.Channel{
|
||||
Alias: data.Alias,
|
||||
Name: data.Name,
|
||||
Description: data.Description,
|
||||
AccountID: user.ID,
|
||||
Type: models.ChannelTypeCommon,
|
||||
IsPublic: data.IsPublic,
|
||||
IsCommunity: data.IsCommunity,
|
||||
Members: []models.ChannelMember{
|
||||
{AccountID: user.ID, PowerLevel: 100},
|
||||
},
|
||||
}
|
||||
|
||||
if realm != nil {
|
||||
channel.RealmID = &realm.ID
|
||||
}
|
||||
|
||||
channel, err := services.NewChannel(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func editChannel(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
id, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
IsPublic bool `json:"is_public"`
|
||||
IsCommunity bool `json:"is_community"`
|
||||
NewBelongsRealm *string `json:"new_belongs_realm"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx := database.C.Where("id = ?", id)
|
||||
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
if info, err := authkit.GetRealmMember(gap.Nx, val.ID, user.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of that realm then can edit channel related to it")
|
||||
} else if info.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can edit channel related to it")
|
||||
} else {
|
||||
tx = tx.Where("realm_id = ?", val.ID)
|
||||
}
|
||||
} else {
|
||||
tx = tx.Where("realm_id IS NULL")
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := tx.First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if channel.RealmID != nil {
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of this channel to edit it")
|
||||
} else if member.PowerLevel < 100 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be channel admin to edit it")
|
||||
}
|
||||
}
|
||||
|
||||
if data.NewBelongsRealm != nil {
|
||||
if *data.NewBelongsRealm == "global" {
|
||||
channel.RealmID = nil
|
||||
} else {
|
||||
realm, err := authkit.GetRealmByAlias(gap.Nx, *data.NewBelongsRealm)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("requested channel with realm, but realm was not found: %v", err))
|
||||
} else {
|
||||
if info, err := authkit.GetRealmMember(gap.Nx, realm.ID, user.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of that realm then can transfer channel related to it")
|
||||
} else if info.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can transfer channel related to it")
|
||||
} else {
|
||||
channel.RealmID = &realm.ID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
channel.Alias = data.Alias
|
||||
channel.Name = data.Name
|
||||
channel.Description = data.Description
|
||||
channel.IsPublic = data.IsPublic
|
||||
channel.IsCommunity = data.IsCommunity
|
||||
|
||||
channel, err := services.EditChannel(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func deleteChannel(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
id, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
tx := database.C.Where("id = ?", id)
|
||||
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
if info, err := authkit.GetRealmMember(gap.Nx, val.ID, user.ID); err != nil {
|
||||
return fmt.Errorf("you must be a part of that realm then can delete channel related to it")
|
||||
} else if info.PowerLevel < 50 {
|
||||
return fmt.Errorf("you must be a moderator of that realm then can delete channel related to it")
|
||||
} else {
|
||||
tx = tx.Where("realm_id = ?", val.ID)
|
||||
}
|
||||
} else {
|
||||
tx = tx.Where("(account_id = ? OR type = ?) AND realm_id IS NULL", user.ID, models.ChannelTypeDirect)
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := tx.First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if channel.Type == models.ChannelTypeDirect {
|
||||
if member, err := services.GetChannelMember(user, channel.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must related to this direct message if you want delete it")
|
||||
} else if member.PowerLevel < 100 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of this direct message if you want delete it")
|
||||
}
|
||||
}
|
||||
|
||||
if err := services.DeleteChannel(channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
84
pkg/internal/web/api/direct_channels_api.go
Normal file
84
pkg/internal/web/api/direct_channels_api.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func createDirectChannel(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,lowercase,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
RelatedUser uint `json:"related_user"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else if err = services.GetChannelAliasAvailability(data.Alias); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
var realm *authm.Realm
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
if info, err := authkit.GetRealmMember(gap.Nx, val.ID, user.ID); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a part of that realm then can create channel related to it")
|
||||
} else if info.PowerLevel < 50 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you must be a moderator of that realm then can create channel related to it")
|
||||
} else {
|
||||
realm = &val
|
||||
}
|
||||
}
|
||||
|
||||
relatedUser, err := authkit.GetUser(gap.Nx, data.RelatedUser)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("unable to find related user: %v", err))
|
||||
}
|
||||
|
||||
if ch, err := services.GetDirectChannelByUser(user, relatedUser); err == nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("you already have a direct with that user #%d", ch.ID))
|
||||
}
|
||||
|
||||
if err := authkit.EnsureUserPermGranted(gap.Nx, user.ID, relatedUser.ID, "ChannelAdd", true); err != nil {
|
||||
return fmt.Errorf("unable to add user into your channel due to access denied: %v", err)
|
||||
}
|
||||
|
||||
channel := models.Channel{
|
||||
Alias: data.Alias,
|
||||
Name: data.Name,
|
||||
Description: data.Description,
|
||||
IsPublic: false,
|
||||
IsCommunity: false,
|
||||
AccountID: user.ID,
|
||||
Type: models.ChannelTypeDirect,
|
||||
Members: []models.ChannelMember{
|
||||
{AccountID: user.ID, PowerLevel: 100},
|
||||
{AccountID: relatedUser.ID, PowerLevel: 100},
|
||||
},
|
||||
}
|
||||
|
||||
if realm != nil {
|
||||
channel.RealmID = &realm.ID
|
||||
}
|
||||
|
||||
channel, err = services.NewChannel(channel)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
168
pkg/internal/web/api/events_api.go
Normal file
168
pkg/internal/web/api/events_api.go
Normal file
@@ -0,0 +1,168 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getEvent(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
id, _ := c.ParamsInt("eventId")
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if _, _, err := services.GetAvailableChannel(channel.ID, user); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("you need join the channel before you read the messages: %v", err))
|
||||
}
|
||||
|
||||
event, err := services.GetEvent(channel.ID, uint(id))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(event)
|
||||
}
|
||||
|
||||
func listEvent(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if _, _, err := services.GetAvailableChannel(channel.ID, user); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("you need join the channel before you read the messages: %v", err))
|
||||
}
|
||||
|
||||
count := services.CountEvent(channel)
|
||||
events, err := services.ListEvent(channel, take, offset)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": events,
|
||||
})
|
||||
}
|
||||
|
||||
func checkHasNewEvent(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
pivot := c.QueryInt("pivot", 0)
|
||||
alias := c.Params("channel")
|
||||
|
||||
if pivot < 1 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "pivot must be greater than zero")
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, err = services.GetChannelWithAlias(alias, val.ID)
|
||||
} else {
|
||||
channel, err = services.GetChannelWithAlias(alias)
|
||||
}
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if _, _, err := services.GetAvailableChannel(channel.ID, user); err != nil {
|
||||
return fiber.NewError(fiber.StatusForbidden, fmt.Sprintf("you need join the channel before you read the messages: %v", err))
|
||||
}
|
||||
|
||||
var count int64
|
||||
if err = database.C.
|
||||
Where("channel_id = ?", channel.ID).
|
||||
Where("id > ?", pivot).
|
||||
Model(&models.Event{}).Count(&count).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.JSON(fiber.Map{
|
||||
"up_to_date": lo.Ternary(count > 0, false, true),
|
||||
"count": count,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newRawEvent(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureGrantedPerm(c, "CreateMessagingRawEvent", true); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Uuid string `json:"uuid" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
Body map[string]any `json:"body"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else if len(data.Uuid) < 36 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "message uuid was not valid")
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
var member models.ChannelMember
|
||||
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, member, err = services.GetChannelIdentity(alias, user.ID, val)
|
||||
} else {
|
||||
channel, member, err = services.GetChannelIdentity(alias, user.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if member.PowerLevel < 0 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "you have not enough permission to send message")
|
||||
}
|
||||
|
||||
event := models.Event{
|
||||
Uuid: data.Uuid,
|
||||
Body: data.Body,
|
||||
Type: data.Type,
|
||||
Sender: member,
|
||||
Channel: channel,
|
||||
ChannelID: channel.ID,
|
||||
SenderID: member.ID,
|
||||
}
|
||||
|
||||
if event, err = services.NewEvent(event); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(event)
|
||||
}
|
160
pkg/internal/web/api/events_message_api.go
Normal file
160
pkg/internal/web/api/events_message_api.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
func newMessageEvent(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Uuid string `json:"uuid" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
Body models.EventMessageBody `json:"body"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else if len(data.Uuid) < 36 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "message uuid was not valid")
|
||||
}
|
||||
|
||||
data.Body.Text = strings.TrimSpace(data.Body.Text)
|
||||
if len(data.Body.Text) == 0 && len(data.Body.Attachments) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "empty message was not allowed")
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
var member models.ChannelMember
|
||||
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, member, err = services.GetChannelIdentity(alias, user.ID, val)
|
||||
} else {
|
||||
channel, member, err = services.GetChannelIdentity(alias, user.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if member.PowerLevel < 0 {
|
||||
return fiber.NewError(fiber.StatusForbidden, "unable to send message, access denied")
|
||||
}
|
||||
|
||||
var parsed map[string]any
|
||||
raw, _ := jsoniter.Marshal(data.Body)
|
||||
_ = jsoniter.Unmarshal(raw, &parsed)
|
||||
|
||||
event := models.Event{
|
||||
Uuid: data.Uuid,
|
||||
Body: parsed,
|
||||
Type: data.Type,
|
||||
Sender: member,
|
||||
Channel: channel,
|
||||
QuoteEventID: data.Body.QuoteEventID,
|
||||
RelatedEventID: data.Body.RelatedEventID,
|
||||
ChannelID: channel.ID,
|
||||
SenderID: member.ID,
|
||||
}
|
||||
|
||||
if event, err = services.NewEvent(event); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(event)
|
||||
}
|
||||
|
||||
func editMessageEvent(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
messageId, _ := c.ParamsInt("messageId", 0)
|
||||
|
||||
var data struct {
|
||||
Uuid string `json:"uuid" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
Body models.EventMessageBody `json:"body"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(data.Body.Text) == 0 && len(data.Body.Attachments) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "you cannot send an empty message")
|
||||
}
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
var member models.ChannelMember
|
||||
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, member, err = services.GetChannelIdentity(alias, user.ID, val)
|
||||
} else {
|
||||
channel, member, err = services.GetChannelIdentity(alias, user.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var event models.Event
|
||||
if event, err = services.GetEventWithSender(channel, member, uint(messageId)); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
event, err = services.EditMessage(event, data.Body)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(event)
|
||||
}
|
||||
|
||||
func deleteMessageEvent(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
alias := c.Params("channel")
|
||||
messageId, _ := c.ParamsInt("messageId", 0)
|
||||
|
||||
var err error
|
||||
var channel models.Channel
|
||||
var member models.ChannelMember
|
||||
|
||||
if val, ok := c.Locals("realm").(authm.Realm); ok {
|
||||
channel, member, err = services.GetChannelIdentity(alias, user.ID, val)
|
||||
} else {
|
||||
channel, member, err = services.GetChannelIdentity(alias, user.ID)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var event models.Event
|
||||
if event, err = services.GetEventWithSender(channel, member, uint(messageId)); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
event, err = services.DeleteMessage(event)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(event)
|
||||
}
|
59
pkg/internal/web/api/index.go
Normal file
59
pkg/internal/web/api/index.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func MapAPIs(app *fiber.App, baseURL string) {
|
||||
api := app.Group(baseURL).Name("API")
|
||||
{
|
||||
quick := api.Group("/quick")
|
||||
{
|
||||
quick.Post("/:channelId/reply/:eventId", quickReply)
|
||||
}
|
||||
|
||||
api.Get("/channels/me", listOwnedChannelGlobalWide)
|
||||
api.Get("/channels/me/available", listAvailableChannelGlobalWide)
|
||||
|
||||
channels := api.Group("/channels/:realm").Use(realmMiddleware).Name("Channels API")
|
||||
{
|
||||
channels.Get("/", listChannel)
|
||||
channels.Get("/public", listPublicChannel)
|
||||
channels.Get("/me", listOwnedChannel)
|
||||
channels.Get("/me/available", listAvailableChannel)
|
||||
channels.Get("/:channel", getChannel)
|
||||
channels.Get("/:channel/me", getChannelIdentity)
|
||||
channels.Get("/:channel/members/me", getChannelIdentity)
|
||||
channels.Put("/:channel/me", editChannelIdentity)
|
||||
channels.Put("/:channel/me/notify", editChannelNotifyLevel)
|
||||
channels.Put("/:channel/members/me/notify", editChannelNotifyLevel)
|
||||
channels.Delete("/:channel/me", deleteChannelIdentity)
|
||||
|
||||
channels.Post("/", createChannel)
|
||||
channels.Post("/dm", createDirectChannel)
|
||||
channels.Put("/:channelId", editChannel)
|
||||
channels.Delete("/:channelId", deleteChannel)
|
||||
channels.Get("/:channel/members", listChannelMembers)
|
||||
channels.Post("/:channel/members", addChannelMember)
|
||||
channels.Delete("/:channel/members/:memberId", removeChannelMember)
|
||||
|
||||
channels.Get("/:channel/events", listEvent)
|
||||
channels.Get("/:channel/events/update", checkHasNewEvent)
|
||||
channels.Get("/:channel/events/:eventId", getEvent)
|
||||
channels.Post("/:channel/events", newRawEvent)
|
||||
|
||||
channels.Post("/:channel/messages", newMessageEvent)
|
||||
channels.Put("/:channel/messages/:messageId", editMessageEvent)
|
||||
channels.Delete("/:channel/messages/:messageId", deleteMessageEvent)
|
||||
|
||||
channels.Get("/:channel/calls", listCall)
|
||||
channels.Get("/:channel/calls/ongoing", getOngoingCall)
|
||||
channels.Post("/:channel/calls", startCall)
|
||||
channels.Delete("/:channel/calls/ongoing", endCall)
|
||||
channels.Delete("/:channel/calls/ongoing/participant", kickParticipantInCall)
|
||||
channels.Post("/:channel/calls/ongoing/token", exchangeCallToken)
|
||||
}
|
||||
|
||||
api.Get("/whats-new", getWhatsNew)
|
||||
}
|
||||
}
|
79
pkg/internal/web/api/quick_actions_api.go
Normal file
79
pkg/internal/web/api/quick_actions_api.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/exts"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/models"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
// quickReply is a simplified API for replying to a message
|
||||
// It used in the iOS notification action and others
|
||||
// It did not support all the features of the message event
|
||||
// But it just works
|
||||
func quickReply(c *fiber.Ctx) error {
|
||||
replyTk := c.Query("replyToken")
|
||||
if len(replyTk) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "reply token is required")
|
||||
}
|
||||
|
||||
claims, err := services.ParseReplyToken(replyTk)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("reply token is invaild: %v", err))
|
||||
}
|
||||
|
||||
channelId, _ := c.ParamsInt("channelId", 0)
|
||||
eventId, _ := c.ParamsInt("eventId", 0)
|
||||
|
||||
if claims.EventID != uint(eventId) {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "reply token is invaild, event id mismatch")
|
||||
}
|
||||
|
||||
var data struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Body models.EventMessageBody `json:"body"`
|
||||
}
|
||||
|
||||
if err := exts.BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
} else {
|
||||
data.Body.QuoteEventID = lo.ToPtr(uint(eventId))
|
||||
}
|
||||
|
||||
data.Body.Text = strings.TrimSpace(data.Body.Text)
|
||||
if len(data.Body.Text) == 0 && len(data.Body.Attachments) == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "empty message was not allowed")
|
||||
}
|
||||
|
||||
channel, member, err := services.GetChannelIdentityWithID(uint(channelId), claims.UserID)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("channel / member not found: %v", err.Error()))
|
||||
}
|
||||
|
||||
var parsed map[string]any
|
||||
raw, _ := jsoniter.Marshal(data.Body)
|
||||
_ = jsoniter.Unmarshal(raw, &parsed)
|
||||
|
||||
event, err := services.NewEvent(models.Event{
|
||||
Uuid: uuid.NewString(),
|
||||
Body: parsed,
|
||||
Type: data.Type,
|
||||
Sender: member,
|
||||
Channel: channel,
|
||||
QuoteEventID: data.Body.QuoteEventID,
|
||||
RelatedEventID: data.Body.RelatedEventID,
|
||||
ChannelID: channel.ID,
|
||||
SenderID: member.ID,
|
||||
})
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(event)
|
||||
}
|
23
pkg/internal/web/api/realms_api.go
Normal file
23
pkg/internal/web/api/realms_api.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func realmMiddleware(c *fiber.Ctx) error {
|
||||
realmAlias := c.Params("realm")
|
||||
if len(realmAlias) > 0 && realmAlias != "global" {
|
||||
realm, err := authkit.GetRealmByAlias(gap.Nx, realmAlias)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("requested channel with realm, but realm was not found: %v", err))
|
||||
} else {
|
||||
c.Locals("realm", realm)
|
||||
}
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
31
pkg/internal/web/api/whats_new_api.go
Normal file
31
pkg/internal/web/api/whats_new_api.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
authm "git.solsynth.dev/hypernet/passport/pkg/authkit/models"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getWhatsNew(c *fiber.Ctx) error {
|
||||
if err := sec.EnsureAuthenticated(c); err != nil {
|
||||
return err
|
||||
}
|
||||
user := c.Locals("user").(authm.Account)
|
||||
|
||||
var result []struct {
|
||||
ChannelID uint `json:"channel_id"`
|
||||
UnreadMessageCount int `json:"count"`
|
||||
}
|
||||
if err := database.C.Table("channel_members cm").
|
||||
Select("cm.channel_id, COUNT(m.id) AS unread_message_count").
|
||||
Joins("JOIN events m ON m.channel_id = cm.channel_id").
|
||||
Where("m.id > cm.reading_anchor AND cm.account_id = ?", user.ID).
|
||||
Group("cm.channel_id").
|
||||
Scan(&result).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(result)
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
package server
|
||||
package exts
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
@@ -16,3 +16,7 @@ func BindAndValidate(c *fiber.Ctx, out any) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateStruct(in any) error {
|
||||
return validation.Struct(in)
|
||||
}
|
75
pkg/internal/web/server.go
Normal file
75
pkg/internal/web/server.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"git.solsynth.dev/hypernet/passport/pkg/authkit"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web/api"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/idempotency"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var IReader *sec.InternalTokenReader
|
||||
|
||||
type App struct {
|
||||
app *fiber.App
|
||||
}
|
||||
|
||||
func NewServer() *App {
|
||||
app := fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
EnableIPValidation: true,
|
||||
ServerHeader: "Hypernet.Messaging",
|
||||
AppName: "Hypernet.Messaging",
|
||||
ProxyHeader: fiber.HeaderXForwardedFor,
|
||||
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
|
||||
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
|
||||
BodyLimit: 50 * 1024 * 1024,
|
||||
ReadBufferSize: 5 * 1024 * 1024, // 5MB for large JWT
|
||||
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
|
||||
})
|
||||
|
||||
app.Use(idempotency.New())
|
||||
app.Use(cors.New(cors.Config{
|
||||
AllowCredentials: true,
|
||||
AllowMethods: strings.Join([]string{
|
||||
fiber.MethodGet,
|
||||
fiber.MethodPost,
|
||||
fiber.MethodHead,
|
||||
fiber.MethodOptions,
|
||||
fiber.MethodPut,
|
||||
fiber.MethodDelete,
|
||||
fiber.MethodPatch,
|
||||
}, ","),
|
||||
AllowOriginsFunc: func(origin string) bool {
|
||||
return true
|
||||
},
|
||||
}))
|
||||
|
||||
app.Use(logger.New(logger.Config{
|
||||
Format: "${status} | ${latency} | ${method} ${path}\n",
|
||||
Output: log.Logger,
|
||||
}))
|
||||
|
||||
app.Use(sec.ContextMiddleware(IReader))
|
||||
app.Use(authkit.ParseAccountMiddleware)
|
||||
|
||||
api.MapAPIs(app, "/api")
|
||||
|
||||
return &App{
|
||||
app: app,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *App) Listen() {
|
||||
if err := v.app.Listen(viper.GetString("bind")); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when starting http...")
|
||||
}
|
||||
}
|
90
pkg/main.go
Normal file
90
pkg/main.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"git.solsynth.dev/hypernet/nexus/pkg/nex/sec"
|
||||
"github.com/fatih/color"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/gap"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/grpc"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/services"
|
||||
"github.com/robfig/cron/v3"
|
||||
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/web"
|
||||
|
||||
pkg "git.solsynth.dev/hypernet/messaging/pkg/internal"
|
||||
"git.solsynth.dev/hypernet/messaging/pkg/internal/database"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Booting screen
|
||||
fmt.Println(color.YellowString(" __ __ _\n| \\/ | ___ ___ ___ __ _ __ _(_)_ __ __ _\n| |\\/| |/ _ \\/ __/ __|/ _` |/ _` | | '_ \\ / _` |\n| | | | __/\\__ \\__ \\ (_| | (_| | | | | | (_| |\n|_| |_|\\___||___/___/\\__,_|\\__, |_|_| |_|\\__, |\n |___/ |___/"))
|
||||
fmt.Printf("%s v%s\n", color.New(color.FgHiYellow).Add(color.Bold).Sprintf("Hypernet.Messaging"), pkg.AppVersion)
|
||||
fmt.Printf("The instant messaging service in Hypernet\n")
|
||||
color.HiBlack("=====================================================\n")
|
||||
|
||||
// Configure settings
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("..")
|
||||
viper.SetConfigName("settings")
|
||||
viper.SetConfigType("toml")
|
||||
|
||||
// Load settings
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
log.Panic().Err(err).Msg("An error occurred when loading settings.")
|
||||
}
|
||||
|
||||
// Connect to nexus
|
||||
if err := gap.InitializeToNexus(); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when connecting to nexus...")
|
||||
}
|
||||
|
||||
// Load keypair
|
||||
if reader, err := sec.NewInternalTokenReader(viper.GetString("security.internal_public_key")); err != nil {
|
||||
log.Error().Err(err).Msg("An error occurred when reading internal public key for jwt. Authentication related features will be disabled.")
|
||||
} else {
|
||||
web.IReader = reader
|
||||
log.Info().Msg("Internal jwt public key loaded.")
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
if err := database.NewGorm(); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when connect to database.")
|
||||
} else if err := database.RunMigration(database.C); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when running database auto migration.")
|
||||
}
|
||||
|
||||
// Connect other services
|
||||
services.SetupLiveKit()
|
||||
|
||||
// Server
|
||||
go web.NewServer().Listen()
|
||||
|
||||
go grpc.NewGrpc().Listen()
|
||||
|
||||
// Configure timed tasks
|
||||
quartz := cron.New(cron.WithLogger(cron.VerbosePrintfLogger(&log.Logger)))
|
||||
quartz.AddFunc("@every 60m", services.DoAutoDatabaseCleanup)
|
||||
quartz.AddFunc("@every 30s", services.FlushReadingAnchor)
|
||||
quartz.Start()
|
||||
|
||||
// Messages
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
quartz.Stop()
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
package models
|
||||
|
||||
// Account profiles basically fetched from Hydrogen.Identity
|
||||
// But cache at here for better usage
|
||||
// At the same time this model can make relations between local models
|
||||
type Account struct {
|
||||
BaseModel
|
||||
|
||||
Name string `json:"name"`
|
||||
Nick string `json:"nick"`
|
||||
Avatar string `json:"avatar"`
|
||||
Banner string `json:"banner"`
|
||||
Description string `json:"description"`
|
||||
EmailAddress string `json:"email_address"`
|
||||
PowerLevel int `json:"power_level"`
|
||||
Attachments []Attachment `json:"attachments" gorm:"foreignKey:AuthorID"`
|
||||
Channels []Channel `json:"channels"`
|
||||
ExternalID uint `json:"external_id"`
|
||||
}
|
@@ -1,41 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type AttachmentType = uint8
|
||||
|
||||
const (
|
||||
AttachmentOthers = AttachmentType(iota)
|
||||
AttachmentPhoto
|
||||
AttachmentVideo
|
||||
AttachmentAudio
|
||||
)
|
||||
|
||||
type Attachment struct {
|
||||
BaseModel
|
||||
|
||||
FileID string `json:"file_id"`
|
||||
Filesize int64 `json:"filesize"`
|
||||
Filename string `json:"filename"`
|
||||
Mimetype string `json:"mimetype"`
|
||||
Hashcode string `json:"hashcode"`
|
||||
Type AttachmentType `json:"type"`
|
||||
ExternalUrl string `json:"external_url"`
|
||||
Author Account `json:"author"`
|
||||
MessageID *uint `json:"message_id"`
|
||||
AuthorID uint `json:"author_id"`
|
||||
}
|
||||
|
||||
func (v Attachment) GetStoragePath() string {
|
||||
basepath := viper.GetString("content")
|
||||
return filepath.Join(basepath, v.FileID)
|
||||
}
|
||||
|
||||
func (v Attachment) GetAccessPath() string {
|
||||
return fmt.Sprintf("/api/attachments/o/%s", v.FileID)
|
||||
}
|
@@ -1,17 +0,0 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type JSONMap = datatypes.JSONType[map[string]any]
|
||||
|
||||
type BaseModel struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
package models
|
||||
|
||||
type ChannelType = uint8
|
||||
|
||||
const (
|
||||
ChannelTypeDirect = ChannelType(iota)
|
||||
ChannelTypeRealm
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
BaseModel
|
||||
|
||||
Alias string `json:"alias" gorm:"uniqueIndex"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Members []ChannelMember `json:"members"`
|
||||
Messages []Message `json:"messages"`
|
||||
Type ChannelType `json:"type"`
|
||||
Account Account `json:"account"`
|
||||
AccountID uint `json:"account_id"`
|
||||
RealmID uint `json:"realm_id"`
|
||||
}
|
||||
|
||||
type ChannelMember struct {
|
||||
BaseModel
|
||||
|
||||
ChannelID uint `json:"channel_id"`
|
||||
AccountID uint `json:"account_id"`
|
||||
Channel Channel `json:"channel"`
|
||||
Account Account `json:"account"`
|
||||
|
||||
Messages []Message `json:"messages" gorm:"foreignKey:SenderID"`
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/datatypes"
|
||||
|
||||
type MessageType = uint8
|
||||
|
||||
const (
|
||||
MessageTypeText = MessageType(iota)
|
||||
MessageTypeAudio
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
BaseModel
|
||||
|
||||
Content string `json:"content"`
|
||||
Metadata datatypes.JSONMap `json:"metadata"`
|
||||
Type MessageType `json:"type"`
|
||||
Attachments []Attachment `json:"attachments"`
|
||||
Channel Channel `json:"channel"`
|
||||
Sender ChannelMember `json:"sender"`
|
||||
ReplyID *uint `json:"reply_id"`
|
||||
ReplyTo *Message `json:"reply_to" gorm:"foreignKey:ReplyID"`
|
||||
ChannelID uint `json:"channel_id"`
|
||||
SenderID uint `json:"sender_id"`
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
package models
|
||||
|
||||
import jsoniter "github.com/json-iterator/go"
|
||||
|
||||
type UnifiedCommand struct {
|
||||
Action string `json:"w"`
|
||||
Message string `json:"m"`
|
||||
Payload any `json:"p"`
|
||||
}
|
||||
|
||||
func UnifiedCommandFromError(err error) UnifiedCommand {
|
||||
return UnifiedCommand{
|
||||
Action: "error",
|
||||
Message: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
func (v UnifiedCommand) Marshal() []byte {
|
||||
data, _ := jsoniter.Marshal(v)
|
||||
return data
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
package security
|
||||
|
||||
import "golang.org/x/crypto/bcrypt"
|
||||
|
||||
func HashPassword(raw string) string {
|
||||
data, _ := bcrypt.GenerateFromPassword([]byte(raw), 12)
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func VerifyPassword(text string, password string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(password), []byte(text)) == nil
|
||||
}
|
@@ -1,81 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type PayloadClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
|
||||
Type string `json:"typ"`
|
||||
}
|
||||
|
||||
const (
|
||||
JwtAccessType = "access"
|
||||
JwtRefreshType = "refresh"
|
||||
)
|
||||
|
||||
const (
|
||||
CookieAccessKey = "identity_auth_key"
|
||||
CookieRefreshKey = "identity_refresh_key"
|
||||
)
|
||||
|
||||
func EncodeJwt(id string, typ, sub string, aud []string, exp time.Time) (string, error) {
|
||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS512, PayloadClaims{
|
||||
jwt.RegisteredClaims{
|
||||
Subject: sub,
|
||||
Audience: aud,
|
||||
Issuer: fmt.Sprintf("https://%s", viper.GetString("domain")),
|
||||
ExpiresAt: jwt.NewNumericDate(exp),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
ID: id,
|
||||
},
|
||||
typ,
|
||||
})
|
||||
|
||||
return tk.SignedString([]byte(viper.GetString("secret")))
|
||||
}
|
||||
|
||||
func DecodeJwt(str string) (PayloadClaims, error) {
|
||||
var claims PayloadClaims
|
||||
tk, err := jwt.ParseWithClaims(str, &claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return []byte(viper.GetString("secret")), nil
|
||||
})
|
||||
if err != nil {
|
||||
return claims, err
|
||||
}
|
||||
|
||||
if data, ok := tk.Claims.(*PayloadClaims); ok {
|
||||
return *data, nil
|
||||
} else {
|
||||
return claims, fmt.Errorf("unexpected token payload: not payload claims type")
|
||||
}
|
||||
}
|
||||
|
||||
func SetJwtCookieSet(c *fiber.Ctx, access, refresh string) {
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: CookieAccessKey,
|
||||
Value: access,
|
||||
Domain: viper.GetString("security.cookie_domain"),
|
||||
SameSite: viper.GetString("security.cookie_samesite"),
|
||||
Expires: time.Now().Add(60 * time.Minute),
|
||||
Path: "/",
|
||||
})
|
||||
c.Cookie(&fiber.Cookie{
|
||||
Name: CookieRefreshKey,
|
||||
Value: refresh,
|
||||
Domain: viper.GetString("security.cookie_domain"),
|
||||
SameSite: viper.GetString("security.cookie_samesite"),
|
||||
Expires: time.Now().Add(24 * 30 * time.Hour),
|
||||
Path: "/",
|
||||
})
|
||||
}
|
@@ -1,61 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func readAttachment(c *fiber.Ctx) error {
|
||||
id := c.Params("fileId")
|
||||
basepath := viper.GetString("content")
|
||||
|
||||
return c.SendFile(filepath.Join(basepath, id))
|
||||
}
|
||||
|
||||
func uploadAttachment(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
hashcode := c.FormValue("hashcode")
|
||||
if len(hashcode) != 64 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "please provide a SHA256 hashcode, length should be 64 characters")
|
||||
}
|
||||
file, err := c.FormFile("attachment")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attachment, err := services.NewAttachment(user, file, hashcode)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
if err := c.SaveFile(file, attachment.GetStoragePath()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"info": attachment,
|
||||
"url": attachment.GetAccessPath(),
|
||||
})
|
||||
}
|
||||
|
||||
func deleteAttachment(c *fiber.Ctx) error {
|
||||
id, _ := c.ParamsInt("id", 0)
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
attachment, err := services.GetAttachmentByID(uint(id))
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if attachment.AuthorID != user.ID {
|
||||
return fiber.NewError(fiber.StatusNotFound, "record not created by you")
|
||||
}
|
||||
|
||||
if err := services.DeleteAttachment(attachment); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
@@ -1,55 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/security"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func authMiddleware(c *fiber.Ctx) error {
|
||||
var token string
|
||||
if cookie := c.Cookies(security.CookieAccessKey); len(cookie) > 0 {
|
||||
token = cookie
|
||||
}
|
||||
if header := c.Get(fiber.HeaderAuthorization); len(header) > 0 {
|
||||
tk := strings.Replace(header, "Bearer", "", 1)
|
||||
token = strings.TrimSpace(tk)
|
||||
}
|
||||
if query := c.Query("tk"); len(query) > 0 {
|
||||
token = strings.TrimSpace(query)
|
||||
}
|
||||
|
||||
c.Locals("token", token)
|
||||
|
||||
if err := authFunc(c); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
|
||||
func authFunc(c *fiber.Ctx, overrides ...string) error {
|
||||
var token string
|
||||
if len(overrides) > 0 {
|
||||
token = overrides[0]
|
||||
} else {
|
||||
if tk, ok := c.Locals("token").(string); !ok {
|
||||
return fiber.NewError(fiber.StatusUnauthorized)
|
||||
} else {
|
||||
token = tk
|
||||
}
|
||||
}
|
||||
|
||||
rtk := c.Cookies(security.CookieRefreshKey)
|
||||
if user, atk, rtk, err := services.Authenticate(token, rtk); err == nil {
|
||||
if atk != token {
|
||||
security.SetJwtCookieSet(c, atk, rtk)
|
||||
}
|
||||
c.Locals("principal", user)
|
||||
return nil
|
||||
} else {
|
||||
return fiber.NewError(fiber.StatusUnauthorized, err.Error())
|
||||
}
|
||||
}
|
@@ -1,114 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listChannelMembers(c *fiber.Ctx) error {
|
||||
channelId, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
if members, err := services.ListChannelMember(uint(channelId)); err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
} else {
|
||||
return c.JSON(members)
|
||||
}
|
||||
}
|
||||
|
||||
func inviteChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
channelId, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
var data struct {
|
||||
AccountName string `json:"account_name" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
BaseModel: models.BaseModel{ID: uint(channelId)},
|
||||
AccountID: user.ID,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
Name: data.AccountName,
|
||||
}).First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.InviteChannelMember(account, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func kickChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
channelId, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
var data struct {
|
||||
AccountName string `json:"account_name" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
BaseModel: models.BaseModel{ID: uint(channelId)},
|
||||
AccountID: user.ID,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
var account models.Account
|
||||
if err := database.C.Where(&models.Account{
|
||||
Name: data.AccountName,
|
||||
}).First(&account).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.RemoveChannelMember(account, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func leaveChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
channelId, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
var data struct {
|
||||
AccountName string `json:"account_name" validate:"required"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
BaseModel: models.BaseModel{ID: uint(channelId)},
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if user.ID == channel.AccountID {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "you cannot leave your own channel")
|
||||
}
|
||||
|
||||
if err := services.RemoveChannelMember(user, channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
} else {
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
||||
}
|
@@ -1,122 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getChannel(c *fiber.Ctx) error {
|
||||
alias := c.Params("channel")
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func listChannel(c *fiber.Ctx) error {
|
||||
channels, err := services.ListChannel()
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listOwnedChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
channels, err := services.ListChannelWithUser(user)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func listAvailableChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
channels, err := services.ListChannelIsAvailable(user)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channels)
|
||||
}
|
||||
|
||||
func createChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channel, err := services.NewChannel(user, data.Alias, data.Name, data.Description)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func editChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
var data struct {
|
||||
Alias string `json:"alias" validate:"required,min=4,max=32"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
AccountID: user.ID,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
channel, err := services.EditChannel(channel, data.Alias, data.Name, data.Description)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(channel)
|
||||
}
|
||||
|
||||
func deleteChannel(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
id, _ := c.ParamsInt("channelId", 0)
|
||||
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(&models.Channel{
|
||||
BaseModel: models.BaseModel{ID: uint(id)},
|
||||
AccountID: user.ID,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
if err := services.DeleteChannel(channel); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.SendStatus(fiber.StatusOK)
|
||||
}
|
@@ -1,130 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func listMessage(c *fiber.Ctx) error {
|
||||
take := c.QueryInt("take", 0)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
alias := c.Params("channel")
|
||||
|
||||
channel, err := services.GetChannelWithAlias(alias)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
count := services.CountMessage(channel)
|
||||
messages, err := services.ListMessage(channel, take, offset)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"count": count,
|
||||
"data": messages,
|
||||
})
|
||||
}
|
||||
|
||||
func newTextMessage(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
|
||||
var data struct {
|
||||
Content string `json:"content" validate:"required"`
|
||||
Attachments []models.Attachment `json:"attachments"`
|
||||
ReplyTo *uint `json:"reply_to"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
channel, member, err := services.GetAvailableChannelWithAlias(alias, user)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
message := models.Message{
|
||||
Content: data.Content,
|
||||
Metadata: nil,
|
||||
Sender: member,
|
||||
Channel: channel,
|
||||
ChannelID: channel.ID,
|
||||
SenderID: member.ID,
|
||||
Attachments: data.Attachments,
|
||||
Type: models.MessageTypeText,
|
||||
}
|
||||
|
||||
var replyTo models.Message
|
||||
if data.ReplyTo != nil {
|
||||
if err := database.C.Where("id = ?", data.ReplyTo).First(&replyTo).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("message to reply was not found: %v", err))
|
||||
} else {
|
||||
message.ReplyTo = &replyTo
|
||||
message.ReplyID = &replyTo.ID
|
||||
}
|
||||
}
|
||||
|
||||
if message, err = services.NewMessage(message); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(message)
|
||||
}
|
||||
|
||||
func editMessage(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
messageId, _ := c.ParamsInt("messageId", 0)
|
||||
|
||||
var data struct {
|
||||
Content string `json:"content" validate:"required"`
|
||||
Attachments []models.Attachment `json:"attachments"`
|
||||
}
|
||||
|
||||
if err := BindAndValidate(c, &data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var message models.Message
|
||||
if channel, member, err := services.GetAvailableChannelWithAlias(alias, user); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if message, err = services.GetMessageWithPrincipal(channel, member, uint(messageId)); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
message.Content = data.Content
|
||||
message.Attachments = data.Attachments
|
||||
|
||||
message, err := services.EditMessage(message)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(message)
|
||||
}
|
||||
|
||||
func deleteMessage(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
alias := c.Params("channel")
|
||||
messageId, _ := c.ParamsInt("messageId", 0)
|
||||
|
||||
var message models.Message
|
||||
if channel, member, err := services.GetAvailableChannelWithAlias(alias, user); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
} else if message, err = services.GetMessageWithPrincipal(channel, member, uint(messageId)); err != nil {
|
||||
return fiber.NewError(fiber.StatusNotFound, err.Error())
|
||||
}
|
||||
|
||||
message, err := services.DeleteMessage(message)
|
||||
if err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(message)
|
||||
}
|
@@ -1,119 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
"github.com/gofiber/fiber/v2/middleware/favicon"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/middleware/cache"
|
||||
"github.com/gofiber/fiber/v2/middleware/cors"
|
||||
"github.com/gofiber/fiber/v2/middleware/idempotency"
|
||||
"github.com/gofiber/fiber/v2/middleware/logger"
|
||||
"github.com/gofiber/template/html/v2"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var A *fiber.App
|
||||
|
||||
func NewServer() {
|
||||
templates := html.NewFileSystem(http.FS(pkg.FS), ".gohtml")
|
||||
|
||||
A = fiber.New(fiber.Config{
|
||||
DisableStartupMessage: true,
|
||||
EnableIPValidation: true,
|
||||
ServerHeader: "Hydrogen.Messaging",
|
||||
AppName: "Hydrogen.Messaging",
|
||||
ProxyHeader: fiber.HeaderXForwardedFor,
|
||||
JSONEncoder: jsoniter.ConfigCompatibleWithStandardLibrary.Marshal,
|
||||
JSONDecoder: jsoniter.ConfigCompatibleWithStandardLibrary.Unmarshal,
|
||||
BodyLimit: 50 * 1024 * 1024,
|
||||
EnablePrintRoutes: viper.GetBool("debug.print_routes"),
|
||||
Views: templates,
|
||||
ViewsLayout: "views/index",
|
||||
})
|
||||
|
||||
A.Use(idempotency.New())
|
||||
A.Use(cors.New(cors.Config{
|
||||
AllowCredentials: true,
|
||||
AllowMethods: strings.Join([]string{
|
||||
fiber.MethodGet,
|
||||
fiber.MethodPost,
|
||||
fiber.MethodHead,
|
||||
fiber.MethodOptions,
|
||||
fiber.MethodPut,
|
||||
fiber.MethodDelete,
|
||||
fiber.MethodPatch,
|
||||
}, ","),
|
||||
AllowOriginsFunc: func(origin string) bool {
|
||||
return true
|
||||
},
|
||||
}))
|
||||
|
||||
A.Use(logger.New(logger.Config{
|
||||
Format: "${status} | ${latency} | ${method} ${path}\n",
|
||||
Output: log.Logger,
|
||||
}))
|
||||
|
||||
A.Get("/.well-known", getMetadata)
|
||||
|
||||
api := A.Group("/api").Name("API")
|
||||
{
|
||||
api.Get("/users/me", authMiddleware, getUserinfo)
|
||||
api.Get("/users/:accountId", getOthersInfo)
|
||||
|
||||
api.Get("/attachments/o/:fileId", cache.New(cache.Config{
|
||||
Expiration: 365 * 24 * time.Hour,
|
||||
CacheControl: true,
|
||||
}), readAttachment)
|
||||
api.Post("/attachments", authMiddleware, uploadAttachment)
|
||||
api.Delete("/attachments/:id", authMiddleware, deleteAttachment)
|
||||
|
||||
channels := api.Group("/channels").Name("Channels API")
|
||||
{
|
||||
channels.Get("/", listChannel)
|
||||
channels.Get("/:channel", getChannel)
|
||||
channels.Get("/me", authMiddleware, listOwnedChannel)
|
||||
channels.Get("/me/available", authMiddleware, listAvailableChannel)
|
||||
|
||||
channels.Post("/", authMiddleware, createChannel)
|
||||
channels.Put("/:channelId", authMiddleware, editChannel)
|
||||
channels.Delete("/:channelId", authMiddleware, deleteChannel)
|
||||
|
||||
channels.Get("/:channelId/members", listChannelMembers)
|
||||
channels.Post("/:channelId/invite", authMiddleware, inviteChannel)
|
||||
channels.Post("/:channelId/kick", authMiddleware, kickChannel)
|
||||
channels.Post("/:channelId/leave", authMiddleware, leaveChannel)
|
||||
|
||||
channels.Get("/:channel/messages", listMessage)
|
||||
channels.Post("/:channel/messages", authMiddleware, newTextMessage)
|
||||
channels.Put("/:channel/messages/:messageId", authMiddleware, editMessage)
|
||||
channels.Delete("/:channel/messages/:messageId", authMiddleware, deleteMessage)
|
||||
}
|
||||
|
||||
api.Get("/unified", authMiddleware, websocket.New(unifiedGateway))
|
||||
}
|
||||
|
||||
A.Use(favicon.New(favicon.Config{
|
||||
FileSystem: http.FS(pkg.FS),
|
||||
File: "views/favicon.png",
|
||||
URL: "/favicon.png",
|
||||
}))
|
||||
|
||||
A.Get("/", func(c *fiber.Ctx) error {
|
||||
return c.Render("views/open", fiber.Map{
|
||||
"frontend": viper.GetString("frontend"),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func Listen() {
|
||||
if err := A.Listen(viper.GetString("bind")); err != nil {
|
||||
log.Fatal().Err(err).Msg("An error occurred when starting server...")
|
||||
}
|
||||
}
|
@@ -1,48 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/services"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func unifiedGateway(c *websocket.Conn) {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
// Push connection
|
||||
services.WsConn[user.ID] = append(services.WsConn[user.ID], c)
|
||||
|
||||
// Event loop
|
||||
var task models.UnifiedCommand
|
||||
|
||||
var messageType int
|
||||
var packet []byte
|
||||
var err error
|
||||
|
||||
for {
|
||||
if messageType, packet, err = c.ReadMessage(); err != nil {
|
||||
break
|
||||
} else if err := jsoniter.Unmarshal(packet, &task); err != nil {
|
||||
_ = c.WriteMessage(messageType, models.UnifiedCommand{
|
||||
Action: "error",
|
||||
Message: "unable to unmarshal your command, requires json request",
|
||||
}.Marshal())
|
||||
continue
|
||||
}
|
||||
|
||||
message := services.DealCommand(task, user)
|
||||
|
||||
if message != nil {
|
||||
if err = c.WriteMessage(messageType, message.Marshal()); err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pop connection
|
||||
services.WsConn[user.ID] = lo.Filter(services.WsConn[user.ID], func(item *websocket.Conn, idx int) bool {
|
||||
return item != c
|
||||
})
|
||||
}
|
@@ -1,33 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func getUserinfo(c *fiber.Ctx) error {
|
||||
user := c.Locals("principal").(models.Account)
|
||||
|
||||
var data models.Account
|
||||
if err := database.C.
|
||||
Where(&models.Account{BaseModel: models.BaseModel{ID: user.ID}}).
|
||||
First(&data).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(data)
|
||||
}
|
||||
|
||||
func getOthersInfo(c *fiber.Ctx) error {
|
||||
accountId := c.Params("accountId")
|
||||
|
||||
var data models.Account
|
||||
if err := database.C.
|
||||
Where(&models.Account{Name: accountId}).
|
||||
First(&data).Error; err != nil {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
return c.JSON(data)
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func getMetadata(c *fiber.Ctx) error {
|
||||
return c.JSON(fiber.Map{
|
||||
"name": viper.GetString("name"),
|
||||
"domain": viper.GetString("domain"),
|
||||
"components": fiber.Map{
|
||||
"identity": viper.GetString("identity.endpoint"),
|
||||
},
|
||||
})
|
||||
}
|
@@ -1,40 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/identity/pkg/grpc/proto"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/grpc"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func GetAccountFriend(userId, relatedId uint, status int) (*proto.FriendshipResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
return grpc.Friendships.GetFriendship(ctx, &proto.FriendshipTwoSideLookupRequest{
|
||||
AccountId: uint64(userId),
|
||||
RelatedId: uint64(relatedId),
|
||||
Status: uint32(status),
|
||||
})
|
||||
}
|
||||
|
||||
func NotifyAccount(user models.Account, subject, content string, realtime bool, links ...*proto.NotifyLink) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
_, err := grpc.Notify.NotifyUser(ctx, &proto.NotifyRequest{
|
||||
ClientId: viper.GetString("identity.client_id"),
|
||||
ClientSecret: viper.GetString("identity.client_secret"),
|
||||
Subject: subject,
|
||||
Content: content,
|
||||
Links: links,
|
||||
RecipientId: uint64(user.ExternalID),
|
||||
IsRealtime: realtime,
|
||||
IsImportant: false,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
@@ -1,127 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func GetAttachmentByID(id uint) (models.Attachment, error) {
|
||||
var attachment models.Attachment
|
||||
if err := database.C.Where(models.Attachment{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
}).First(&attachment).Error; err != nil {
|
||||
return attachment, err
|
||||
}
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
func GetAttachmentByUUID(fileId string) (models.Attachment, error) {
|
||||
var attachment models.Attachment
|
||||
if err := database.C.Where(models.Attachment{
|
||||
FileID: fileId,
|
||||
}).First(&attachment).Error; err != nil {
|
||||
return attachment, err
|
||||
}
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
func GetAttachmentByHashcode(hashcode string) (models.Attachment, error) {
|
||||
var attachment models.Attachment
|
||||
if err := database.C.Where(models.Attachment{
|
||||
Hashcode: hashcode,
|
||||
}).First(&attachment).Error; err != nil {
|
||||
return attachment, err
|
||||
}
|
||||
return attachment, nil
|
||||
}
|
||||
|
||||
func NewAttachment(user models.Account, header *multipart.FileHeader, hashcode string) (models.Attachment, error) {
|
||||
var attachment models.Attachment
|
||||
existsAttachment, err := GetAttachmentByHashcode(hashcode)
|
||||
if err != nil {
|
||||
// Upload the new file
|
||||
attachment = models.Attachment{
|
||||
FileID: uuid.NewString(),
|
||||
Filesize: header.Size,
|
||||
Filename: header.Filename,
|
||||
Hashcode: hashcode,
|
||||
Mimetype: "unknown/unknown",
|
||||
Type: models.AttachmentOthers,
|
||||
AuthorID: user.ID,
|
||||
}
|
||||
|
||||
// Open file
|
||||
file, err := header.Open()
|
||||
if err != nil {
|
||||
return attachment, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Detect mimetype
|
||||
fileHeader := make([]byte, 512)
|
||||
_, err = file.Read(fileHeader)
|
||||
if err != nil {
|
||||
return attachment, err
|
||||
}
|
||||
attachment.Mimetype = http.DetectContentType(fileHeader)
|
||||
|
||||
switch strings.Split(attachment.Mimetype, "/")[0] {
|
||||
case "image":
|
||||
attachment.Type = models.AttachmentPhoto
|
||||
case "video":
|
||||
attachment.Type = models.AttachmentVideo
|
||||
case "audio":
|
||||
attachment.Type = models.AttachmentAudio
|
||||
default:
|
||||
attachment.Type = models.AttachmentOthers
|
||||
}
|
||||
} else {
|
||||
// Instant upload, build link with the exists file
|
||||
attachment = models.Attachment{
|
||||
FileID: existsAttachment.FileID,
|
||||
Filesize: header.Size,
|
||||
Filename: header.Filename,
|
||||
Hashcode: hashcode,
|
||||
Mimetype: existsAttachment.Mimetype,
|
||||
Type: existsAttachment.Type,
|
||||
AuthorID: user.ID,
|
||||
}
|
||||
}
|
||||
|
||||
// Save into database
|
||||
err = database.C.Save(&attachment).Error
|
||||
|
||||
return attachment, err
|
||||
}
|
||||
|
||||
func DeleteAttachment(item models.Attachment) error {
|
||||
var dupeCount int64
|
||||
if err := database.C.
|
||||
Where(&models.Attachment{Hashcode: item.Hashcode}).
|
||||
Model(&models.Attachment{}).
|
||||
Count(&dupeCount).Error; err != nil {
|
||||
dupeCount = -1
|
||||
}
|
||||
|
||||
if err := database.C.Delete(&item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if dupeCount != -1 && dupeCount <= 1 {
|
||||
// Safe for deletion the physics file
|
||||
basepath := viper.GetString("content")
|
||||
fullpath := filepath.Join(basepath, item.FileID)
|
||||
|
||||
os.Remove(fullpath)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@@ -1,76 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"git.solsynth.dev/hydrogen/identity/pkg/grpc/proto"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/grpc"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func LinkAccount(userinfo *proto.Userinfo) (models.Account, error) {
|
||||
var account models.Account
|
||||
if userinfo == nil {
|
||||
return account, fmt.Errorf("remote userinfo was not found")
|
||||
}
|
||||
if err := database.C.Where(&models.Account{
|
||||
ExternalID: uint(userinfo.Id),
|
||||
}).First(&account).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
account = models.Account{
|
||||
Name: userinfo.Name,
|
||||
Nick: userinfo.Nick,
|
||||
Avatar: userinfo.Avatar,
|
||||
Banner: userinfo.Banner,
|
||||
Description: userinfo.GetDescription(),
|
||||
EmailAddress: userinfo.Email,
|
||||
PowerLevel: 0,
|
||||
ExternalID: uint(userinfo.Id),
|
||||
}
|
||||
return account, database.C.Save(&account).Error
|
||||
}
|
||||
return account, err
|
||||
}
|
||||
|
||||
prev := account
|
||||
account.Name = userinfo.Name
|
||||
account.Nick = userinfo.Nick
|
||||
account.Avatar = userinfo.Avatar
|
||||
account.Banner = userinfo.Banner
|
||||
account.Description = userinfo.GetDescription()
|
||||
account.EmailAddress = userinfo.Email
|
||||
|
||||
var err error
|
||||
if !reflect.DeepEqual(account, prev) {
|
||||
err = database.C.Save(&account).Error
|
||||
}
|
||||
|
||||
return account, err
|
||||
}
|
||||
|
||||
func Authenticate(atk, rtk string) (models.Account, string, string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
var user models.Account
|
||||
reply, err := grpc.Auth.Authenticate(ctx, &proto.AuthRequest{
|
||||
AccessToken: atk,
|
||||
RefreshToken: &rtk,
|
||||
})
|
||||
if err != nil {
|
||||
return user, reply.GetAccessToken(), reply.GetRefreshToken(), err
|
||||
} else if !reply.IsValid {
|
||||
return user, reply.GetAccessToken(), reply.GetRefreshToken(), fmt.Errorf("invalid authorization context")
|
||||
}
|
||||
|
||||
user, err = LinkAccount(reply.Userinfo)
|
||||
|
||||
return user, reply.GetAccessToken(), reply.GetRefreshToken(), err
|
||||
}
|
@@ -1,184 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func GetChannel(id uint) (models.Channel, error) {
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(models.Channel{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
}).First(&channel).Error; err != nil {
|
||||
return channel, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func GetChannelWithAlias(alias string) (models.Channel, error) {
|
||||
var channel models.Channel
|
||||
if err := database.C.Where(models.Channel{
|
||||
Alias: alias,
|
||||
}).First(&channel).Error; err != nil {
|
||||
return channel, err
|
||||
}
|
||||
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func GetAvailableChannelWithAlias(alias string, user models.Account) (models.Channel, models.ChannelMember, error) {
|
||||
var err error
|
||||
var member models.ChannelMember
|
||||
var channel models.Channel
|
||||
if channel, err = GetChannelWithAlias(alias); err != nil {
|
||||
return channel, member, err
|
||||
}
|
||||
|
||||
if err := database.C.Where(models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
ChannelID: channel.ID,
|
||||
}).First(&member).Error; err != nil {
|
||||
return channel, member, fmt.Errorf("channel principal not found: %v", err.Error())
|
||||
}
|
||||
|
||||
return channel, member, nil
|
||||
}
|
||||
|
||||
func GetAvailableChannel(id uint, user models.Account) (models.Channel, models.ChannelMember, error) {
|
||||
var err error
|
||||
var member models.ChannelMember
|
||||
var channel models.Channel
|
||||
if channel, err = GetChannel(id); err != nil {
|
||||
return channel, member, err
|
||||
}
|
||||
|
||||
if err := database.C.Where(models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
ChannelID: channel.ID,
|
||||
}).First(&member).Error; err != nil {
|
||||
return channel, member, fmt.Errorf("channel principal not found: %v", err.Error())
|
||||
}
|
||||
|
||||
return channel, member, nil
|
||||
}
|
||||
|
||||
func ListChannel() ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
if err := database.C.Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func ListChannelWithUser(user models.Account) ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
if err := database.C.Where(&models.Channel{AccountID: user.ID}).Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func ListChannelIsAvailable(user models.Account) ([]models.Channel, error) {
|
||||
var channels []models.Channel
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
AccountID: user.ID,
|
||||
}).Find(&members).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
idx := lo.Map(members, func(item models.ChannelMember, index int) uint {
|
||||
return item.ChannelID
|
||||
})
|
||||
|
||||
if err := database.C.Where("id IN ?", idx).Find(&channels).Error; err != nil {
|
||||
return channels, err
|
||||
}
|
||||
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
func NewChannel(user models.Account, alias, name, description string) (models.Channel, error) {
|
||||
channel := models.Channel{
|
||||
Alias: alias,
|
||||
Name: name,
|
||||
Description: description,
|
||||
AccountID: user.ID,
|
||||
Members: []models.ChannelMember{
|
||||
{AccountID: user.ID},
|
||||
},
|
||||
}
|
||||
|
||||
err := database.C.Save(&channel).Error
|
||||
|
||||
return channel, err
|
||||
}
|
||||
|
||||
func ListChannelMember(channelId uint) ([]models.ChannelMember, error) {
|
||||
var members []models.ChannelMember
|
||||
|
||||
if err := database.C.
|
||||
Where(&models.ChannelMember{ChannelID: channelId}).
|
||||
Preload("Account").
|
||||
Find(&members).Error; err != nil {
|
||||
return members, err
|
||||
}
|
||||
|
||||
return members, nil
|
||||
}
|
||||
|
||||
func InviteChannelMember(user models.Account, target models.Channel) error {
|
||||
if _, err := GetAccountFriend(user.ID, target.AccountID, 1); err != nil {
|
||||
return fmt.Errorf("you only can invite your friends to your channel")
|
||||
}
|
||||
|
||||
member := models.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
err := database.C.Save(&member).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func AddChannelMember(user models.Account, target models.Channel) error {
|
||||
member := models.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
AccountID: user.ID,
|
||||
}
|
||||
|
||||
err := database.C.Save(&member).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func RemoveChannelMember(user models.Account, target models.Channel) error {
|
||||
var member models.ChannelMember
|
||||
|
||||
if err := database.C.Where(&models.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
AccountID: user.ID,
|
||||
}).First(&member).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return database.C.Delete(&member).Error
|
||||
}
|
||||
|
||||
func EditChannel(channel models.Channel, alias, name, description string) (models.Channel, error) {
|
||||
channel.Alias = alias
|
||||
channel.Name = name
|
||||
channel.Description = description
|
||||
|
||||
err := database.C.Save(&channel).Error
|
||||
|
||||
return channel, err
|
||||
}
|
||||
|
||||
func DeleteChannel(channel models.Channel) error {
|
||||
return database.C.Delete(&channel).Error
|
||||
}
|
@@ -1,28 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"github.com/gofiber/contrib/websocket"
|
||||
)
|
||||
|
||||
var WsConn = make(map[uint][]*websocket.Conn)
|
||||
|
||||
func CheckOnline(user models.Account) bool {
|
||||
return len(WsConn[user.ID]) > 0
|
||||
}
|
||||
|
||||
func PushCommand(userId uint, task models.UnifiedCommand) {
|
||||
for _, conn := range WsConn[userId] {
|
||||
_ = conn.WriteMessage(1, task.Marshal())
|
||||
}
|
||||
}
|
||||
|
||||
func DealCommand(task models.UnifiedCommand, user models.Account) *models.UnifiedCommand {
|
||||
switch task.Action {
|
||||
default:
|
||||
return &models.UnifiedCommand{
|
||||
Action: "error",
|
||||
Message: "command not found",
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,51 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"net/textproto"
|
||||
|
||||
"github.com/jordan-wright/email"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func SendMail(target string, subject string, content string) error {
|
||||
mail := &email.Email{
|
||||
To: []string{target},
|
||||
From: viper.GetString("mailer.name"),
|
||||
Subject: subject,
|
||||
Text: []byte(content),
|
||||
Headers: textproto.MIMEHeader{},
|
||||
}
|
||||
return mail.SendWithTLS(
|
||||
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
|
||||
smtp.PlainAuth(
|
||||
"",
|
||||
viper.GetString("mailer.username"),
|
||||
viper.GetString("mailer.password"),
|
||||
viper.GetString("mailer.smtp_host"),
|
||||
),
|
||||
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
|
||||
)
|
||||
}
|
||||
|
||||
func SendMailHTML(target string, subject string, content string) error {
|
||||
mail := &email.Email{
|
||||
To: []string{target},
|
||||
From: viper.GetString("mailer.name"),
|
||||
Subject: subject,
|
||||
HTML: []byte(content),
|
||||
Headers: textproto.MIMEHeader{},
|
||||
}
|
||||
return mail.SendWithTLS(
|
||||
fmt.Sprintf("%s:%d", viper.GetString("mailer.smtp_host"), viper.GetInt("mailer.smtp_port")),
|
||||
smtp.PlainAuth(
|
||||
"",
|
||||
viper.GetString("mailer.username"),
|
||||
viper.GetString("mailer.password"),
|
||||
viper.GetString("mailer.smtp_host"),
|
||||
),
|
||||
&tls.Config{ServerName: viper.GetString("mailer.smtp_host")},
|
||||
)
|
||||
}
|
@@ -1,144 +0,0 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/database"
|
||||
"git.solsynth.dev/hydrogen/messaging/pkg/models"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func CountMessage(channel models.Channel) int64 {
|
||||
var count int64
|
||||
if err := database.C.Where(models.Message{
|
||||
ChannelID: channel.ID,
|
||||
}).Model(&models.Message{}).Count(&count).Error; err != nil {
|
||||
return 0
|
||||
} else {
|
||||
return count
|
||||
}
|
||||
}
|
||||
|
||||
func ListMessage(channel models.Channel, take int, offset int) ([]models.Message, error) {
|
||||
if take > 100 {
|
||||
take = 100
|
||||
}
|
||||
|
||||
var messages []models.Message
|
||||
if err := database.C.
|
||||
Where(models.Message{
|
||||
ChannelID: channel.ID,
|
||||
}).Limit(take).Offset(offset).
|
||||
Order("created_at DESC").
|
||||
Preload("Attachments").
|
||||
Preload("ReplyTo").
|
||||
Preload("ReplyTo.Sender").
|
||||
Preload("ReplyTo.Sender.Account").
|
||||
Preload("Sender").
|
||||
Preload("Sender.Account").
|
||||
Find(&messages).Error; err != nil {
|
||||
return messages, err
|
||||
} else {
|
||||
return messages, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetMessage(channel models.Channel, id uint) (models.Message, error) {
|
||||
var message models.Message
|
||||
if err := database.C.
|
||||
Where(models.Message{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
ChannelID: channel.ID,
|
||||
}).
|
||||
Preload("ReplyTo").
|
||||
Preload("ReplyTo.Sender").
|
||||
Preload("ReplyTo.Sender.Account").
|
||||
Preload("Attachments").
|
||||
Preload("Sender").
|
||||
Preload("Sender.Account").
|
||||
First(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else {
|
||||
return message, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetMessageWithPrincipal(channel models.Channel, member models.ChannelMember, id uint) (models.Message, error) {
|
||||
var message models.Message
|
||||
if err := database.C.Where(models.Message{
|
||||
BaseModel: models.BaseModel{ID: id},
|
||||
ChannelID: channel.ID,
|
||||
SenderID: member.ID,
|
||||
}).First(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else {
|
||||
return message, nil
|
||||
}
|
||||
}
|
||||
|
||||
func NewMessage(message models.Message) (models.Message, error) {
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: message.ChannelID,
|
||||
}).Preload("Account").Find(&members).Error; err == nil {
|
||||
for _, member := range members {
|
||||
if member.ID != message.Sender.ID {
|
||||
err = NotifyAccount(member.Account, "New message at "+message.Channel.Name, message.Content, true)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("An error occurred when trying notify user.")
|
||||
}
|
||||
}
|
||||
|
||||
message, _ = GetMessage(message.Channel, message.ID)
|
||||
PushCommand(member.AccountID, models.UnifiedCommand{
|
||||
Action: "messages.new",
|
||||
Payload: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func EditMessage(message models.Message) (models.Message, error) {
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Save(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: message.ChannelID,
|
||||
}).Find(&members).Error; err == nil {
|
||||
message, _ = GetMessage(models.Channel{
|
||||
BaseModel: models.BaseModel{ID: message.Channel.ID},
|
||||
}, message.ID)
|
||||
for _, member := range members {
|
||||
PushCommand(member.AccountID, models.UnifiedCommand{
|
||||
Action: "messages.update",
|
||||
Payload: message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
func DeleteMessage(message models.Message) (models.Message, error) {
|
||||
prev, _ := GetMessage(models.Channel{
|
||||
BaseModel: models.BaseModel{ID: message.Channel.ID},
|
||||
}, message.ID)
|
||||
|
||||
var members []models.ChannelMember
|
||||
if err := database.C.Delete(&message).Error; err != nil {
|
||||
return message, err
|
||||
} else if err = database.C.Where(models.ChannelMember{
|
||||
ChannelID: message.ChannelID,
|
||||
}).Find(&members).Error; err == nil {
|
||||
for _, member := range members {
|
||||
PushCommand(member.AccountID, models.UnifiedCommand{
|
||||
Action: "messages.burnt",
|
||||
Payload: prev,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 72 KiB |
@@ -1,28 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap" rel="stylesheet">
|
||||
|
||||
<title>Hydrogen.Interactive</title>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: Roboto Mono, monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{{embed}}
|
||||
</body>
|
||||
</html>
|
@@ -1,60 +0,0 @@
|
||||
<div class="container">
|
||||
<div>
|
||||
<img src="/favicon.png" width="128" height="128" alt="Icon"/>
|
||||
|
||||
<p class="caption text-blinking">Launching Solian... 🚀</p>
|
||||
<p class="description">
|
||||
Hold on a second... <br/>
|
||||
We are redirecting you to our application...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function redirect() {
|
||||
window.location.href = {{ .frontend }}
|
||||
}
|
||||
|
||||
setTimeout(() => redirect(), 1850)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.caption {
|
||||
margin-top: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-blinking {
|
||||
animation: text-blinking ease-in-out infinite 1.5s;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 4px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@keyframes text-blinking {
|
||||
0% {
|
||||
opacity: 100%;
|
||||
}
|
||||
50% {
|
||||
opacity: 10%;
|
||||
}
|
||||
100% {
|
||||
opacity: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -1,37 +1,25 @@
|
||||
name = "Solarplaza"
|
||||
maintainer = "SmartSheep Studio"
|
||||
|
||||
frontend = "https://lian.solsynth.dev"
|
||||
id = "messaging01"
|
||||
|
||||
bind = "0.0.0.0:8447"
|
||||
domain = "feed.smartsheep.studio"
|
||||
secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi"
|
||||
grpc_bind = "0.0.0.0:7447"
|
||||
|
||||
content = "uploads"
|
||||
nexus_addr = "localhost:7001"
|
||||
|
||||
[performance]
|
||||
passive_user_optimize = true
|
||||
|
||||
[debug]
|
||||
database = false
|
||||
print_routes = false
|
||||
|
||||
[identity]
|
||||
client_id = "solarecho"
|
||||
client_secret = "Z9k9AFTj^p"
|
||||
endpoint = "https://id.solsynth.dev"
|
||||
grpc_endpoint = "id.solsynth.dev:7444"
|
||||
|
||||
[mailer]
|
||||
name = "Alphabot <alphabot@smartsheep.studio>"
|
||||
smtp_host = "smtp.exmail.qq.com"
|
||||
smtp_port = 465
|
||||
username = "alphabot@smartsheep.studio"
|
||||
password = "gz937Zxxzfcd9SeH"
|
||||
[calling]
|
||||
api_key = "APIZwKRLAWaWa8d"
|
||||
api_secret = "wdu3fnKDwlsIW17tLhlRKpx275kPJGwRKMC7JADNaXU"
|
||||
endpoint = "hydrogen-kcvgdwpe.livekit.cloud"
|
||||
token_duration = 43200
|
||||
empty_timeout_duration = 600
|
||||
max_participants = 20
|
||||
|
||||
[security]
|
||||
cookie_domain = "localhost"
|
||||
cookie_samesite = "Lax"
|
||||
access_token_duration = 300
|
||||
refresh_token_duration = 2592000
|
||||
|
||||
[database]
|
||||
dsn = "host=localhost dbname=hy_messaging port=5432 sslmode=disable"
|
||||
prefix = "messaging_"
|
||||
internal_public_key = "keys/internal_public_key.pem"
|
||||
reply_token_secret = "QSu8iYYLDkGj10H3FtNI7OabpGl8N7Rg6rBagofQN4Uza3nIrXpVzDEfAiU1G2Yn"
|
||||
|
Reference in New Issue
Block a user