diff --git a/.gitignore b/.gitignore
index e47c96ac..aaa695b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,166 @@ server.json
# TypeScript cache
*.tsbuildinfo
+
+### Go ###
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+
+### Node ###
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+### Node Patch ###
+# Serverless Webpack directories
+.webpack/
+
+# Optional stylelint cache
+
+# SvelteKit build / generate output
+.svelte-kit
diff --git a/.idea/php.xml b/.idea/php.xml
new file mode 100644
index 00000000..8250cf43
--- /dev/null
+++ b/.idea/php.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 6ed7d025..634818f6 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -5,9 +5,15 @@
"name": "Client",
"type": "go",
"request": "launch",
- "mode": "auto",
+ "mode": "debug",
"program": "${workspaceFolder}/client/cmd/client"
- }
-
+ },
+ {
+ "name": "Server",
+ "type": "go",
+ "request": "launch",
+ "mode": "debug",
+ "program": "${workspaceFolder}/server/cmd/server"
+ }
]
}
diff --git a/.zed/debug.json b/.zed/debug.json
new file mode 100644
index 00000000..171501df
--- /dev/null
+++ b/.zed/debug.json
@@ -0,0 +1,16 @@
+// Project-local debug tasks
+//
+// For more documentation on how to configure debug tasks,
+// see: https://zed.dev/docs/debugger
+[
+ {
+ "label": "Run server",
+ "adapter": "Delve",
+ "request": "launch",
+ "mode": "debug",
+ // For Delve, the program can be a package name
+ "program": "./server/cmd/server"
+ // "args": [],
+ // "buildFlags": [],
+ }
+]
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
new file mode 100644
index 00000000..b37f291a
--- /dev/null
+++ b/DEVELOPMENT.md
@@ -0,0 +1,83 @@
+# Development
+
+This document outlines development tools and practices used for FriendNet.
+
+## Philosophy
+
+These are the philosophies that guide FriendNet development.
+
+### Independence
+
+FriendNet's main guiding principle is independence. A client should only ever need a server to operate, and a server
+should not need any external components to operate.
+
+Some examples of this principle:
+ - Server binaries are statically-linked and do not depend on the existence of any global system libraries
+ - Servers store their state in a local SQLite database instead of an external database server
+ - Clients use STUN servers provided by the server, including the server itself
+
+### Compatibility
+
+When it comes to the protocol and its implementation, the main guiding principle is compatibility. The very first
+release of the FriendNet client should be able to connect to the latest release of the server, and vice versa. In the
+absence of support for any feature, clients and servers should gracefully degrade to simpler functionality. Users often
+have very good reasons for not updating, and we should not force them to do so.
+
+In short, **we do not break compatibility**.
+
+### Wariness of Dependencies
+
+For the actual source code, we try to be prudent about dependencies. External dependencies are a liability, and we
+should resort to the Go standard library and our own code as much as possible. For example, we did not need the full
+range of STUN functionality, so we implemented our own stripped-down STUN client and server. If a dependency is
+desirable, consider whether we can vendor it. For example, the [ahocorasick](ahocorasick) implementation we use for
+search filtering is vendored.
+
+First-party dependencies like the `golang.org/x/` modules or the `@solid-primitives` are fine because they are official
+extensions of libraries we already use.
+
+Dependencies outside already-trusted groups must be considered with scrutiny. Updates must also be justified, not
+applied blindly.
+
+### Documentation
+
+From the protocol to the source code, FriendNet must be documented thoroughly. We must not assume we will be the only
+implementation of the protocol, and we want to make it as easy as possible for others to both use and adapt or implement
+FriendNet. Documentation must also be [user-facing](website/docs) as much as possible.
+
+### No CGo
+
+We do not use CGo. It makes cross-compilation hard and static linking even harder. It also adds additional build
+dependencies for maintainers and packagers. CGo is banned in this project with no exceptions.
+
+## Client Development
+
+Developing the client involves two separate components: the client daemon and the web UI. The client daemon is written
+in Go and exposes its functionality over gRPC (or gRPC-Web) and ConnectRPC. The web UI is written in TypeScript using
+Solid.js as its UI framework, and communicating with the client daemon via ConnectRPC.
+
+Separating the client's logic with its UI has a few benefits:
+ - No need for a separate client implementation for headless use on a server (Soulseek, in contrast, requires a
+[different client](https://github.com/slskd/slskd) for headless use)
+ - Clear separation of concerns
+ - Easier implementation of alternate UIs, both web and native
+ - Easier automation
+
+### Web UI Development
+
+If you are already running a client, you can connect to it using a development build of the web UI.
+
+To start running the web UI in development mode, run `npm run dev` in the [webui](webui) directory. You should see a URL
+show up in the terminal.
+
+Copy the debug URL, then append the following to the end of it
+
+`?token=&rpc=`
+
+where `` is the token printed at client startup, and `` is the RPC URL printed at
+client startup. Your final URL should look something like this:
+
+`http://localhost:5173?token=Aan7RbpZavqMjz3mo1wXT38YGAkUqvDigyccyRb3iPI&rpc=http://127.0.0.1:20042`
+
+This specifies the RPC URL and token required to communicate with the client daemon. You can now enjoy hot reloading
+and all the other benefits of Solid.js and Vite's development tools.
diff --git a/Makefile b/Makefile
index be3572fe..f5f52e19 100644
--- a/Makefile
+++ b/Makefile
@@ -22,6 +22,7 @@
run-rpcclient \
server-docker \
server-docker-publish \
+ website \
release-artifacts
help:
@@ -103,6 +104,9 @@ server-docker:
server-docker-publish:
make server-docker && docker push git.termer.net/termer/friendnet-server:latest
+website:
+ cd website && npm install && npm run build
+
release-artifacts:
rm -rf /tmp/fn-release
mkdir /tmp/fn-release
diff --git a/adminui/go.mod b/adminui/go.mod
index 587c250f..8a4e2d76 100644
--- a/adminui/go.mod
+++ b/adminui/go.mod
@@ -1,3 +1,3 @@
module friendnet.org/adminui
-go 1.26.2
+go 1.26.5
diff --git a/adminui/pb/serverrpc/v1/rpc_pb.ts b/adminui/pb/serverrpc/v1/rpc_pb.ts
index dadec738..b5c6e454 100644
--- a/adminui/pb/serverrpc/v1/rpc_pb.ts
+++ b/adminui/pb/serverrpc/v1/rpc_pb.ts
@@ -2,15 +2,15 @@
// @generated from file pb/serverrpc/v1/rpc.proto (package pb.serverrpc.v1, syntax proto3)
/* eslint-disable */
-import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
-import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
+import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
+import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file pb/serverrpc/v1/rpc.proto.
*/
export const file_pb_serverrpc_v1_rpc: GenFile = /*@__PURE__*/
- fileDesc("ChlwYi9zZXJ2ZXJycGMvdjEvcnBjLnByb3RvEg9wYi5zZXJ2ZXJycGMudjEiMwoIUm9vbUluZm8SDAoEbmFtZRgBIAEoCRIZChFvbmxpbmVfdXNlcl9jb3VudBgCIAEoDSIiCg5PbmxpbmVVc2VySW5mbxIQCgh1c2VybmFtZRgBIAEoCSIfCgtBY2NvdW50SW5mbxIQCgh1c2VybmFtZRgBIAEoCSIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKgAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSNwoDcnBjGAIgASgLMioucGIuc2VydmVycnBjLnYxLkdldFNlcnZlckluZm9SZXNwb25zZS5ScGMaPQoDUnBjEhcKD2FsbG93ZWRfbWV0aG9kcxgBIAMoCRIdChVyZXF1aXJlc19iZWFyZXJfdG9rZW4YAiABKAgiEQoPR2V0Um9vbXNSZXF1ZXN0IjwKEEdldFJvb21zUmVzcG9uc2USKAoFcm9vbXMYASADKAsyGS5wYi5zZXJ2ZXJycGMudjEuUm9vbUluZm8iIgoSR2V0Um9vbUluZm9SZXF1ZXN0EgwKBG5hbWUYASABKAkiPgoTR2V0Um9vbUluZm9SZXNwb25zZRInCgRyb29tGAEgASgLMhkucGIuc2VydmVycnBjLnYxLlJvb21JbmZvIiUKFUdldE9ubGluZVVzZXJzUmVxdWVzdBIMCgRyb29tGAEgASgJIkgKFkdldE9ubGluZVVzZXJzUmVzcG9uc2USLgoFdXNlcnMYASADKAsyHy5wYi5zZXJ2ZXJycGMudjEuT25saW5lVXNlckluZm8iOgoYR2V0T25saW5lVXNlckluZm9SZXF1ZXN0EgwKBHJvb20YASABKAkSEAoIdXNlcm5hbWUYAiABKAkiSgoZR2V0T25saW5lVXNlckluZm9SZXNwb25zZRItCgR1c2VyGAEgASgLMh8ucGIuc2VydmVycnBjLnYxLk9ubGluZVVzZXJJbmZvIiIKEkdldEFjY291bnRzUmVxdWVzdBIMCgRyb29tGAEgASgJIkUKE0dldEFjY291bnRzUmVzcG9uc2USLgoIYWNjb3VudHMYASADKAsyHC5wYi5zZXJ2ZXJycGMudjEuQWNjb3VudEluZm8iIQoRQ3JlYXRlUm9vbVJlcXVlc3QSDAoEbmFtZRgBIAEoCSI9ChJDcmVhdGVSb29tUmVzcG9uc2USJwoEcm9vbRgBIAEoCzIZLnBiLnNlcnZlcnJwYy52MS5Sb29tSW5mbyIhChFEZWxldGVSb29tUmVxdWVzdBIMCgRuYW1lGAEgASgJIhQKEkRlbGV0ZVJvb21SZXNwb25zZSJIChRDcmVhdGVBY2NvdW50UmVxdWVzdBIMCgRyb29tGAEgASgJEhAKCHVzZXJuYW1lGAIgASgJEhAKCHBhc3N3b3JkGAMgASgJIn4KFUNyZWF0ZUFjY291bnRSZXNwb25zZRItCgdhY2NvdW50GAEgASgLMhwucGIuc2VydmVycnBjLnYxLkFjY291bnRJbmZvEh8KEmdlbmVyYXRlZF9wYXNzd29yZBgCIAEoCUgAiAEBQhUKE19nZW5lcmF0ZWRfcGFzc3dvcmQiNgoURGVsZXRlQWNjb3VudFJlcXVlc3QSDAoEcm9vbRgBIAEoCRIQCgh1c2VybmFtZRgCIAEoCSIXChVEZWxldGVBY2NvdW50UmVzcG9uc2UiUAocVXBkYXRlQWNjb3VudFBhc3N3b3JkUmVxdWVzdBIMCgRyb29tGAEgASgJEhAKCHVzZXJuYW1lGAIgASgJEhAKCHBhc3N3b3JkGAMgASgJIlcKHVVwZGF0ZUFjY291bnRQYXNzd29yZFJlc3BvbnNlEh8KEmdlbmVyYXRlZF9wYXNzd29yZBgBIAEoCUgAiAEBQhUKE19nZW5lcmF0ZWRfcGFzc3dvcmQyxAgKEFNlcnZlclJwY1NlcnZpY2USYAoNR2V0U2VydmVySW5mbxIlLnBiLnNlcnZlcnJwYy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBomLnBiLnNlcnZlcnJwYy52MS5HZXRTZXJ2ZXJJbmZvUmVzcG9uc2UiABJRCghHZXRSb29tcxIgLnBiLnNlcnZlcnJwYy52MS5HZXRSb29tc1JlcXVlc3QaIS5wYi5zZXJ2ZXJycGMudjEuR2V0Um9vbXNSZXNwb25zZSIAEloKC0dldFJvb21JbmZvEiMucGIuc2VydmVycnBjLnYxLkdldFJvb21JbmZvUmVxdWVzdBokLnBiLnNlcnZlcnJwYy52MS5HZXRSb29tSW5mb1Jlc3BvbnNlIgASZQoOR2V0T25saW5lVXNlcnMSJi5wYi5zZXJ2ZXJycGMudjEuR2V0T25saW5lVXNlcnNSZXF1ZXN0GicucGIuc2VydmVycnBjLnYxLkdldE9ubGluZVVzZXJzUmVzcG9uc2UiADABEmwKEUdldE9ubGluZVVzZXJJbmZvEikucGIuc2VydmVycnBjLnYxLkdldE9ubGluZVVzZXJJbmZvUmVxdWVzdBoqLnBiLnNlcnZlcnJwYy52MS5HZXRPbmxpbmVVc2VySW5mb1Jlc3BvbnNlIgASWgoLR2V0QWNjb3VudHMSIy5wYi5zZXJ2ZXJycGMudjEuR2V0QWNjb3VudHNSZXF1ZXN0GiQucGIuc2VydmVycnBjLnYxLkdldEFjY291bnRzUmVzcG9uc2UiABJXCgpDcmVhdGVSb29tEiIucGIuc2VydmVycnBjLnYxLkNyZWF0ZVJvb21SZXF1ZXN0GiMucGIuc2VydmVycnBjLnYxLkNyZWF0ZVJvb21SZXNwb25zZSIAElcKCkRlbGV0ZVJvb20SIi5wYi5zZXJ2ZXJycGMudjEuRGVsZXRlUm9vbVJlcXVlc3QaIy5wYi5zZXJ2ZXJycGMudjEuRGVsZXRlUm9vbVJlc3BvbnNlIgASYAoNQ3JlYXRlQWNjb3VudBIlLnBiLnNlcnZlcnJwYy52MS5DcmVhdGVBY2NvdW50UmVxdWVzdBomLnBiLnNlcnZlcnJwYy52MS5DcmVhdGVBY2NvdW50UmVzcG9uc2UiABJgCg1EZWxldGVBY2NvdW50EiUucGIuc2VydmVycnBjLnYxLkRlbGV0ZUFjY291bnRSZXF1ZXN0GiYucGIuc2VydmVycnBjLnYxLkRlbGV0ZUFjY291bnRSZXNwb25zZSIAEngKFVVwZGF0ZUFjY291bnRQYXNzd29yZBItLnBiLnNlcnZlcnJwYy52MS5VcGRhdGVBY2NvdW50UGFzc3dvcmRSZXF1ZXN0Gi4ucGIuc2VydmVycnBjLnYxLlVwZGF0ZUFjY291bnRQYXNzd29yZFJlc3BvbnNlIgBCIlogZnJpZW5kbmV0Lm9yZy9wcm90b2NvbC9zZXJ2ZXJycGNiBnByb3RvMw");
+ fileDesc("ChlwYi9zZXJ2ZXJycGMvdjEvcnBjLnByb3RvEg9wYi5zZXJ2ZXJycGMudjEiMwoIUm9vbUluZm8SDAoEbmFtZRgBIAEoCRIZChFvbmxpbmVfdXNlcl9jb3VudBgCIAEoDSIiCg5PbmxpbmVVc2VySW5mbxIQCgh1c2VybmFtZRgBIAEoCSIfCgtBY2NvdW50SW5mbxIQCgh1c2VybmFtZRgBIAEoCSIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKgAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSNwoDcnBjGAIgASgLMioucGIuc2VydmVycnBjLnYxLkdldFNlcnZlckluZm9SZXNwb25zZS5ScGMaPQoDUnBjEhcKD2FsbG93ZWRfbWV0aG9kcxgBIAMoCRIdChVyZXF1aXJlc19iZWFyZXJfdG9rZW4YAiABKAgiEQoPR2V0Um9vbXNSZXF1ZXN0IjwKEEdldFJvb21zUmVzcG9uc2USKAoFcm9vbXMYASADKAsyGS5wYi5zZXJ2ZXJycGMudjEuUm9vbUluZm8iIgoSR2V0Um9vbUluZm9SZXF1ZXN0EgwKBG5hbWUYASABKAkiPgoTR2V0Um9vbUluZm9SZXNwb25zZRInCgRyb29tGAEgASgLMhkucGIuc2VydmVycnBjLnYxLlJvb21JbmZvIiUKFUdldE9ubGluZVVzZXJzUmVxdWVzdBIMCgRyb29tGAEgASgJIkgKFkdldE9ubGluZVVzZXJzUmVzcG9uc2USLgoFdXNlcnMYASADKAsyHy5wYi5zZXJ2ZXJycGMudjEuT25saW5lVXNlckluZm8iOgoYR2V0T25saW5lVXNlckluZm9SZXF1ZXN0EgwKBHJvb20YASABKAkSEAoIdXNlcm5hbWUYAiABKAkiSgoZR2V0T25saW5lVXNlckluZm9SZXNwb25zZRItCgR1c2VyGAEgASgLMh8ucGIuc2VydmVycnBjLnYxLk9ubGluZVVzZXJJbmZvIiIKEkdldEFjY291bnRzUmVxdWVzdBIMCgRyb29tGAEgASgJIkUKE0dldEFjY291bnRzUmVzcG9uc2USLgoIYWNjb3VudHMYASADKAsyHC5wYi5zZXJ2ZXJycGMudjEuQWNjb3VudEluZm8iIQoRQ3JlYXRlUm9vbVJlcXVlc3QSDAoEbmFtZRgBIAEoCSI9ChJDcmVhdGVSb29tUmVzcG9uc2USJwoEcm9vbRgBIAEoCzIZLnBiLnNlcnZlcnJwYy52MS5Sb29tSW5mbyIhChFEZWxldGVSb29tUmVxdWVzdBIMCgRuYW1lGAEgASgJIhQKEkRlbGV0ZVJvb21SZXNwb25zZSJIChRDcmVhdGVBY2NvdW50UmVxdWVzdBIMCgRyb29tGAEgASgJEhAKCHVzZXJuYW1lGAIgASgJEhAKCHBhc3N3b3JkGAMgASgJIn4KFUNyZWF0ZUFjY291bnRSZXNwb25zZRItCgdhY2NvdW50GAEgASgLMhwucGIuc2VydmVycnBjLnYxLkFjY291bnRJbmZvEh8KEmdlbmVyYXRlZF9wYXNzd29yZBgCIAEoCUgAiAEBQhUKE19nZW5lcmF0ZWRfcGFzc3dvcmQiNgoURGVsZXRlQWNjb3VudFJlcXVlc3QSDAoEcm9vbRgBIAEoCRIQCgh1c2VybmFtZRgCIAEoCSIXChVEZWxldGVBY2NvdW50UmVzcG9uc2UiUAocVXBkYXRlQWNjb3VudFBhc3N3b3JkUmVxdWVzdBIMCgRyb29tGAEgASgJEhAKCHVzZXJuYW1lGAIgASgJEhAKCHBhc3N3b3JkGAMgASgJIlcKHVVwZGF0ZUFjY291bnRQYXNzd29yZFJlc3BvbnNlEh8KEmdlbmVyYXRlZF9wYXNzd29yZBgBIAEoCUgAiAEBQhUKE19nZW5lcmF0ZWRfcGFzc3dvcmQiVQoPQmxhY2tsaXN0UG9saWN5Eg8KB2tleXdvcmQYASABKAkSMQoEbW9kZRgCIAEoDjIjLnBiLnNlcnZlcnJwYy52MS5CbGFja2xpc3RNYXRjaE1vZGUibQobQWRkQmxhY2tsaXN0UG9saWNpZXNSZXF1ZXN0EhEKBHJvb20YASABKAlIAIgBARIyCghwb2xpY2llcxgCIAMoCzIgLnBiLnNlcnZlcnJwYy52MS5CbGFja2xpc3RQb2xpY3lCBwoFX3Jvb20iHgocQWRkQmxhY2tsaXN0UG9saWNpZXNSZXNwb25zZSJOCh5SZW1vdmVCbGFja2xpc3RQb2xpY2llc1JlcXVlc3QSEQoEcm9vbRgBIAEoCUgAiAEBEhAKCHBvbGljaWVzGAIgAygJQgcKBV9yb29tIiEKH1JlbW92ZUJsYWNrbGlzdFBvbGljaWVzUmVzcG9uc2UiOgocTGlzdEJsYWNrbGlzdFBvbGljaWVzUmVxdWVzdBIRCgRyb29tGAEgASgJSACIAQFCBwoFX3Jvb20iUwodTGlzdEJsYWNrbGlzdFBvbGljaWVzUmVzcG9uc2USMgoIcG9saWNpZXMYASADKAsyIC5wYi5zZXJ2ZXJycGMudjEuQmxhY2tsaXN0UG9saWN5Kp4BChJCbGFja2xpc3RNYXRjaE1vZGUSJAogQkxBQ0tMSVNUX01BVENIX01PREVfVU5TUEVDSUZJRUQQABIiCh5CTEFDS0xJU1RfTUFUQ0hfTU9ERV9TVUJTVFJJTkcQARIeChpCTEFDS0xJU1RfTUFUQ0hfTU9ERV9XSE9MRRACEh4KGkJMQUNLTElTVF9NQVRDSF9NT0RFX1JFR0VYEAMytQsKEFNlcnZlclJwY1NlcnZpY2USYAoNR2V0U2VydmVySW5mbxIlLnBiLnNlcnZlcnJwYy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBomLnBiLnNlcnZlcnJwYy52MS5HZXRTZXJ2ZXJJbmZvUmVzcG9uc2UiABJRCghHZXRSb29tcxIgLnBiLnNlcnZlcnJwYy52MS5HZXRSb29tc1JlcXVlc3QaIS5wYi5zZXJ2ZXJycGMudjEuR2V0Um9vbXNSZXNwb25zZSIAEloKC0dldFJvb21JbmZvEiMucGIuc2VydmVycnBjLnYxLkdldFJvb21JbmZvUmVxdWVzdBokLnBiLnNlcnZlcnJwYy52MS5HZXRSb29tSW5mb1Jlc3BvbnNlIgASZQoOR2V0T25saW5lVXNlcnMSJi5wYi5zZXJ2ZXJycGMudjEuR2V0T25saW5lVXNlcnNSZXF1ZXN0GicucGIuc2VydmVycnBjLnYxLkdldE9ubGluZVVzZXJzUmVzcG9uc2UiADABEmwKEUdldE9ubGluZVVzZXJJbmZvEikucGIuc2VydmVycnBjLnYxLkdldE9ubGluZVVzZXJJbmZvUmVxdWVzdBoqLnBiLnNlcnZlcnJwYy52MS5HZXRPbmxpbmVVc2VySW5mb1Jlc3BvbnNlIgASWgoLR2V0QWNjb3VudHMSIy5wYi5zZXJ2ZXJycGMudjEuR2V0QWNjb3VudHNSZXF1ZXN0GiQucGIuc2VydmVycnBjLnYxLkdldEFjY291bnRzUmVzcG9uc2UiABJXCgpDcmVhdGVSb29tEiIucGIuc2VydmVycnBjLnYxLkNyZWF0ZVJvb21SZXF1ZXN0GiMucGIuc2VydmVycnBjLnYxLkNyZWF0ZVJvb21SZXNwb25zZSIAElcKCkRlbGV0ZVJvb20SIi5wYi5zZXJ2ZXJycGMudjEuRGVsZXRlUm9vbVJlcXVlc3QaIy5wYi5zZXJ2ZXJycGMudjEuRGVsZXRlUm9vbVJlc3BvbnNlIgASYAoNQ3JlYXRlQWNjb3VudBIlLnBiLnNlcnZlcnJwYy52MS5DcmVhdGVBY2NvdW50UmVxdWVzdBomLnBiLnNlcnZlcnJwYy52MS5DcmVhdGVBY2NvdW50UmVzcG9uc2UiABJgCg1EZWxldGVBY2NvdW50EiUucGIuc2VydmVycnBjLnYxLkRlbGV0ZUFjY291bnRSZXF1ZXN0GiYucGIuc2VydmVycnBjLnYxLkRlbGV0ZUFjY291bnRSZXNwb25zZSIAEngKFVVwZGF0ZUFjY291bnRQYXNzd29yZBItLnBiLnNlcnZlcnJwYy52MS5VcGRhdGVBY2NvdW50UGFzc3dvcmRSZXF1ZXN0Gi4ucGIuc2VydmVycnBjLnYxLlVwZGF0ZUFjY291bnRQYXNzd29yZFJlc3BvbnNlIgASdQoUQWRkQmxhY2tsaXN0UG9saWNpZXMSLC5wYi5zZXJ2ZXJycGMudjEuQWRkQmxhY2tsaXN0UG9saWNpZXNSZXF1ZXN0Gi0ucGIuc2VydmVycnBjLnYxLkFkZEJsYWNrbGlzdFBvbGljaWVzUmVzcG9uc2UiABJ+ChdSZW1vdmVCbGFja2xpc3RQb2xpY2llcxIvLnBiLnNlcnZlcnJwYy52MS5SZW1vdmVCbGFja2xpc3RQb2xpY2llc1JlcXVlc3QaMC5wYi5zZXJ2ZXJycGMudjEuUmVtb3ZlQmxhY2tsaXN0UG9saWNpZXNSZXNwb25zZSIAEngKFUxpc3RCbGFja2xpc3RQb2xpY2llcxItLnBiLnNlcnZlcnJwYy52MS5MaXN0QmxhY2tsaXN0UG9saWNpZXNSZXF1ZXN0Gi4ucGIuc2VydmVycnBjLnYxLkxpc3RCbGFja2xpc3RQb2xpY2llc1Jlc3BvbnNlIgBCIlogZnJpZW5kbmV0Lm9yZy9wcm90b2NvbC9zZXJ2ZXJycGNiBnByb3RvMw");
/**
* RoomInfo is information about a room.
@@ -557,6 +557,189 @@ export type UpdateAccountPasswordResponse = Message<"pb.serverrpc.v1.UpdateAccou
export const UpdateAccountPasswordResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_pb_serverrpc_v1_rpc, 24);
+/**
+ * BlacklistPolicy is a word and the policy for how it will be enforced in a blacklist.
+ *
+ * @generated from message pb.serverrpc.v1.BlacklistPolicy
+ */
+export type BlacklistPolicy = Message<"pb.serverrpc.v1.BlacklistPolicy"> & {
+ /**
+ * The keyword.
+ *
+ * @generated from field: string keyword = 1;
+ */
+ keyword: string;
+
+ /**
+ * How the keyword will be matched.
+ *
+ * @generated from field: pb.serverrpc.v1.BlacklistMatchMode mode = 2;
+ */
+ mode: BlacklistMatchMode;
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.BlacklistPolicy.
+ * Use `create(BlacklistPolicySchema)` to create a new message.
+ */
+export const BlacklistPolicySchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 25);
+
+/**
+ * @generated from message pb.serverrpc.v1.AddBlacklistPoliciesRequest
+ */
+export type AddBlacklistPoliciesRequest = Message<"pb.serverrpc.v1.AddBlacklistPoliciesRequest"> & {
+ /**
+ * Room to enforce this policy. If null, then it is enforced serverwide.
+ *
+ * @generated from field: optional string room = 1;
+ */
+ room?: string;
+
+ /**
+ * The policies to add.
+ *
+ * @generated from field: repeated pb.serverrpc.v1.BlacklistPolicy policies = 2;
+ */
+ policies: BlacklistPolicy[];
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.AddBlacklistPoliciesRequest.
+ * Use `create(AddBlacklistPoliciesRequestSchema)` to create a new message.
+ */
+export const AddBlacklistPoliciesRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 26);
+
+/**
+ * @generated from message pb.serverrpc.v1.AddBlacklistPoliciesResponse
+ */
+export type AddBlacklistPoliciesResponse = Message<"pb.serverrpc.v1.AddBlacklistPoliciesResponse"> & {
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.AddBlacklistPoliciesResponse.
+ * Use `create(AddBlacklistPoliciesResponseSchema)` to create a new message.
+ */
+export const AddBlacklistPoliciesResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 27);
+
+/**
+ * @generated from message pb.serverrpc.v1.RemoveBlacklistPoliciesRequest
+ */
+export type RemoveBlacklistPoliciesRequest = Message<"pb.serverrpc.v1.RemoveBlacklistPoliciesRequest"> & {
+ /**
+ * Room in which this policy is enforced. If null, it is assumed to be serverwide.
+ *
+ * @generated from field: optional string room = 1;
+ */
+ room?: string;
+
+ /**
+ * The policies to remove (identified by their keywords).
+ *
+ * @generated from field: repeated string policies = 2;
+ */
+ policies: string[];
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.RemoveBlacklistPoliciesRequest.
+ * Use `create(RemoveBlacklistPoliciesRequestSchema)` to create a new message.
+ */
+export const RemoveBlacklistPoliciesRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 28);
+
+/**
+ * @generated from message pb.serverrpc.v1.RemoveBlacklistPoliciesResponse
+ */
+export type RemoveBlacklistPoliciesResponse = Message<"pb.serverrpc.v1.RemoveBlacklistPoliciesResponse"> & {
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.RemoveBlacklistPoliciesResponse.
+ * Use `create(RemoveBlacklistPoliciesResponseSchema)` to create a new message.
+ */
+export const RemoveBlacklistPoliciesResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 29);
+
+/**
+ * @generated from message pb.serverrpc.v1.ListBlacklistPoliciesRequest
+ */
+export type ListBlacklistPoliciesRequest = Message<"pb.serverrpc.v1.ListBlacklistPoliciesRequest"> & {
+ /**
+ * If null, it will return only global policies.
+ *
+ * @generated from field: optional string room = 1;
+ */
+ room?: string;
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.ListBlacklistPoliciesRequest.
+ * Use `create(ListBlacklistPoliciesRequestSchema)` to create a new message.
+ */
+export const ListBlacklistPoliciesRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 30);
+
+/**
+ * @generated from message pb.serverrpc.v1.ListBlacklistPoliciesResponse
+ */
+export type ListBlacklistPoliciesResponse = Message<"pb.serverrpc.v1.ListBlacklistPoliciesResponse"> & {
+ /**
+ * The policies.
+ *
+ * @generated from field: repeated pb.serverrpc.v1.BlacklistPolicy policies = 1;
+ */
+ policies: BlacklistPolicy[];
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.ListBlacklistPoliciesResponse.
+ * Use `create(ListBlacklistPoliciesResponseSchema)` to create a new message.
+ */
+export const ListBlacklistPoliciesResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 31);
+
+/**
+ * BlacklistMatchMode are the possible modes for matching a word in a blacklist.
+ *
+ * @generated from enum pb.serverrpc.v1.BlacklistMatchMode
+ */
+export enum BlacklistMatchMode {
+ /**
+ * @generated from enum value: BLACKLIST_MATCH_MODE_UNSPECIFIED = 0;
+ */
+ UNSPECIFIED = 0,
+
+ /**
+ * Matches any substring of the word.
+ *
+ * @generated from enum value: BLACKLIST_MATCH_MODE_SUBSTRING = 1;
+ */
+ SUBSTRING = 1,
+
+ /**
+ * Matches only whole words that match.
+ *
+ * @generated from enum value: BLACKLIST_MATCH_MODE_WHOLE = 2;
+ */
+ WHOLE = 2,
+
+ /**
+ * Matches terms with regex.
+ *
+ * @generated from enum value: BLACKLIST_MATCH_MODE_REGEX = 3;
+ */
+ REGEX = 3,
+}
+
+/**
+ * Describes the enum pb.serverrpc.v1.BlacklistMatchMode.
+ */
+export const BlacklistMatchModeSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_pb_serverrpc_v1_rpc, 0);
+
/**
* ServerRpcService provides an RPC interface to a running FriendNet server.
* It can query state and perform administrative tasks.
@@ -694,6 +877,41 @@ export const ServerRpcService: GenService<{
input: typeof UpdateAccountPasswordRequestSchema;
output: typeof UpdateAccountPasswordResponseSchema;
},
+ /**
+ * AddBlacklistPolicies adds one or more keywords that will be blacklisted from search queries and filenames.
+ * If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ * Returns status code INVALID_ARGUMENT if the keyword is empty or invalid for the specified match mode.
+ *
+ * @generated from rpc pb.serverrpc.v1.ServerRpcService.AddBlacklistPolicies
+ */
+ addBlacklistPolicies: {
+ methodKind: "unary";
+ input: typeof AddBlacklistPoliciesRequestSchema;
+ output: typeof AddBlacklistPoliciesResponseSchema;
+ },
+ /**
+ * RemoveBlacklistPolicies removes one or more keyword policies from the blacklists.
+ * If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ *
+ * @generated from rpc pb.serverrpc.v1.ServerRpcService.RemoveBlacklistPolicies
+ */
+ removeBlacklistPolicies: {
+ methodKind: "unary";
+ input: typeof RemoveBlacklistPoliciesRequestSchema;
+ output: typeof RemoveBlacklistPoliciesResponseSchema;
+ },
+ /**
+ * ListBlacklistPolicies returns a list of currently blacklisted keywords for a room.
+ * If the room is not set, the list returned will contain only serverwide policies.
+ * Returns status code NOT_FOUND if the room does not exist.
+ *
+ * @generated from rpc pb.serverrpc.v1.ServerRpcService.ListBlacklistPolicies
+ */
+ listBlacklistPolicies: {
+ methodKind: "unary";
+ input: typeof ListBlacklistPoliciesRequestSchema;
+ output: typeof ListBlacklistPoliciesResponseSchema;
+ },
}> = /*@__PURE__*/
serviceDesc(file_pb_serverrpc_v1_rpc, 0);
diff --git a/ahocorasick/ahocorasick.go b/ahocorasick/ahocorasick.go
new file mode 100644
index 00000000..b5413d0b
--- /dev/null
+++ b/ahocorasick/ahocorasick.go
@@ -0,0 +1,172 @@
+// This module is taken verbatim from:
+// https://github.com/anknown/ahocorasick
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2015 hanshinan
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+package ahocorasick
+
+import (
+ "fmt"
+)
+
+const FAIL_STATE = -1
+const ROOT_STATE = 1
+
+type Machine struct {
+ trie *DoubleArrayTrie
+ failure []int
+ output map[int]([][]rune)
+}
+
+type Term struct {
+ Pos int
+ Word []rune
+}
+
+func (m *Machine) Build(keywords [][]rune) (err error) {
+ if len(keywords) == 0 {
+ return fmt.Errorf("empty keywords")
+ }
+
+ d := new(Darts)
+
+ trie := new(LinkedListTrie)
+ m.trie, trie, err = d.Build(keywords)
+ if err != nil {
+ return err
+ }
+
+ m.output = make(map[int]([][]rune), 0)
+ for idx, val := range d.Output {
+ m.output[idx] = append(m.output[idx], val)
+ }
+
+ queue := make([](*LinkedListTrieNode), 0)
+ m.failure = make([]int, len(m.trie.Base))
+ for _, c := range trie.Root.Children {
+ m.failure[c.Base] = ROOT_NODE_BASE
+ }
+ queue = append(queue, trie.Root.Children...)
+
+ for {
+ if len(queue) == 0 {
+ break
+ }
+
+ node := queue[0]
+ for _, n := range node.Children {
+ if n.Base == END_NODE_BASE {
+ continue
+ }
+ inState := m.f(node.Base)
+ set_state:
+ outState := m.g(inState, n.Code-ROOT_NODE_BASE)
+ if outState == FAIL_STATE {
+ inState = m.f(inState)
+ goto set_state
+ }
+ if _, ok := m.output[outState]; ok != false {
+ copyOutState := make([][]rune, 0)
+ for _, o := range m.output[outState] {
+ copyOutState = append(copyOutState, o)
+ }
+ m.output[n.Base] = append(copyOutState, m.output[n.Base]...)
+ }
+ m.setF(n.Base, outState)
+ }
+ queue = append(queue, node.Children...)
+ queue = queue[1:]
+ }
+
+ return nil
+}
+
+func (m *Machine) g(inState int, input rune) (outState int) {
+ if inState == FAIL_STATE {
+ return ROOT_STATE
+ }
+
+ t := inState + int(input) + ROOT_NODE_BASE
+ if t >= len(m.trie.Base) {
+ if inState == ROOT_STATE {
+ return ROOT_STATE
+ }
+ return FAIL_STATE
+ }
+ if inState == m.trie.Check[t] {
+ return m.trie.Base[t]
+ }
+
+ if inState == ROOT_STATE {
+ return ROOT_STATE
+ }
+
+ return FAIL_STATE
+}
+
+func (m *Machine) f(index int) (state int) {
+ return m.failure[index]
+}
+
+func (m *Machine) setF(inState, outState int) {
+ m.failure[inState] = outState
+}
+
+func (m *Machine) MultiPatternSearch(content []rune, returnImmediately bool) [](*Term) {
+ terms := make([](*Term), 0)
+
+ state := ROOT_STATE
+ for pos, c := range content {
+ start:
+ if m.g(state, c) == FAIL_STATE {
+ state = m.f(state)
+ goto start
+ } else {
+ state = m.g(state, c)
+ if val, ok := m.output[state]; ok != false {
+ for _, word := range val {
+ term := new(Term)
+ term.Pos = pos - len(word) + 1
+ term.Word = word
+ terms = append(terms, term)
+ if returnImmediately {
+ return terms
+ }
+ }
+ }
+ }
+ }
+
+ return terms
+}
+
+func (m *Machine) ExactSearch(content []rune) [](*Term) {
+ if m.trie.ExactMatchSearch(content, 0) {
+ t := new(Term)
+ t.Word = content
+ t.Pos = 0
+ return [](*Term){t}
+ }
+
+ return nil
+}
diff --git a/ahocorasick/darts.go b/ahocorasick/darts.go
new file mode 100644
index 00000000..2ffaa428
--- /dev/null
+++ b/ahocorasick/darts.go
@@ -0,0 +1,339 @@
+// This module is taken verbatim from:
+// https://github.com/anknown/darts
+//
+// The MIT License (MIT)
+// Copyright (c) 2015 hanshinan
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+package ahocorasick
+
+import (
+ "fmt"
+ "sort"
+)
+
+const RESIZE_DELTA = 64
+const END_NODE_BASE = -1
+const ROOT_NODE_BASE = 1
+const ROOT_NODE_INDEX = 0
+
+// Linked List Trie
+type LinkedListTrieNode struct {
+ Code rune
+ Depth, Left, Right, Index, Base int
+ SubKey []rune
+ Children [](*LinkedListTrieNode)
+}
+
+type LinkedListTrie struct {
+ Root *LinkedListTrieNode
+}
+
+// Double Array Trie
+type DoubleArrayTrie struct {
+ Base []int
+ Check []int
+}
+
+type dartsKey []rune
+type dartsKeySlice []dartsKey
+
+type Darts struct {
+ dat *DoubleArrayTrie
+ llt *LinkedListTrie
+ used []bool
+ nextCheckPos int
+ key dartsKeySlice
+ Output map[int]([]rune)
+}
+
+func (k dartsKeySlice) Len() int {
+ return len(k)
+}
+
+func (k dartsKeySlice) Less(i, j int) bool {
+ iKey, jKey := k[i], k[j]
+ iLen, jLen := len(iKey), len(jKey)
+
+ var pos int = 0
+ for {
+ if pos < iLen && pos < jLen {
+ if iKey[pos] < jKey[pos] {
+ return true
+ } else if iKey[pos] > jKey[pos] {
+ return false
+ }
+ } else {
+ if iLen < jLen {
+ return true
+ } else {
+ return false
+ }
+ }
+ pos++
+ }
+
+ return false
+}
+
+func (k dartsKeySlice) Swap(i, j int) {
+ k[i], k[j] = k[j], k[i]
+}
+
+func (llt *LinkedListTrie) printTrie(n *LinkedListTrieNode) {
+ for i := 0; i < n.Depth; i++ {
+ fmt.Printf("\t")
+ }
+ for _, c := range n.Children {
+ llt.printTrie(c)
+ }
+}
+
+func (llt *LinkedListTrie) PrintTrie() {
+ llt.printTrie(llt.Root)
+}
+
+func (dat *DoubleArrayTrie) PrintTrie() {
+ fmt.Printf("+-----+-----+-----+\n")
+ fmt.Printf("|%5s|%5s|%5s|\n", "id", "base", "check")
+ for idx, _ := range dat.Base {
+ fmt.Printf("+-----+-----+-----+\n")
+ fmt.Printf("|%5d|%5d|%5d|\n", idx, dat.Base[idx], dat.Check[idx])
+ }
+ fmt.Printf("+-----+-----+-----+\n")
+}
+
+func (d *Darts) Build(keywords [][]rune) (*DoubleArrayTrie, *LinkedListTrie, error) {
+ if len(keywords) == 0 {
+ return nil, nil, fmt.Errorf("empty keywords")
+ }
+
+ d.dat = new(DoubleArrayTrie)
+ d.resize(RESIZE_DELTA)
+
+ for _, keyword := range keywords {
+ var dk dartsKey = keyword
+ d.key = append(d.key, dk)
+ }
+ sort.Sort(d.key)
+
+ d.Output = make(map[int]([]rune), len(d.key))
+ d.dat.Base[0] = ROOT_NODE_BASE
+ d.nextCheckPos = 0
+
+ d.llt = new(LinkedListTrie)
+ d.llt.Root = new(LinkedListTrieNode)
+ d.llt.Root.Depth = 0
+ d.llt.Root.Left = 0
+ d.llt.Root.Right = len(keywords)
+ d.llt.Root.SubKey = nil
+ d.llt.Root.Index = ROOT_NODE_INDEX
+
+ siblings, err := d.fetch(d.llt.Root)
+ if err != nil {
+ return nil, nil, err
+ }
+ for idx, ns := range siblings {
+ if ns.Code > 0 {
+ siblings[idx].SubKey = append(d.llt.Root.SubKey, ns.Code-ROOT_NODE_BASE)
+ }
+ }
+
+ _, err = d.insert(siblings)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return d.dat, d.llt, nil
+}
+
+func (d *Darts) resize(size int) {
+ d.dat.Base = append(d.dat.Base, make([]int, (size-len(d.dat.Base)))...)
+ d.dat.Check = append(d.dat.Check, make([]int, (size-len(d.dat.Check)))...)
+
+ d.used = append(d.used, make([]bool, (size-len(d.used)))...)
+}
+
+func (d *Darts) fetch(parent *LinkedListTrieNode) (siblings [](*LinkedListTrieNode), err error) {
+ siblings = make([](*LinkedListTrieNode), 0, 2)
+
+ var prev rune = 0
+
+ for i := parent.Left; i < parent.Right; i++ {
+
+ if len(d.key[i]) < parent.Depth {
+ continue
+ }
+
+ tmp := d.key[i]
+
+ var cur rune = 0
+ if len(d.key[i]) != parent.Depth {
+ cur = tmp[parent.Depth] + 1
+ }
+
+ if prev > cur {
+ return nil, fmt.Errorf("fetch error")
+ }
+
+ if cur != prev || len(siblings) == 0 {
+ var subKey []rune
+ if cur != 0 {
+ subKey = append(parent.SubKey, cur-ROOT_NODE_BASE)
+ } else {
+ subKey = parent.SubKey
+ }
+
+ tmpNode := new(LinkedListTrieNode)
+ tmpNode.Depth = parent.Depth + 1
+ tmpNode.Code = cur
+ tmpNode.Left = i
+ tmpNode.SubKey = make([]rune, len(subKey))
+ copy(tmpNode.SubKey, subKey)
+ if len(siblings) != 0 {
+ siblings[len(siblings)-1].Right = i
+ }
+ siblings = append(siblings, tmpNode)
+ if len(parent.Children) != 0 {
+ parent.Children[len(parent.Children)-1].Right = i
+ }
+ parent.Children = append(parent.Children, tmpNode)
+ }
+
+ prev = cur
+ }
+
+ if len(siblings) != 0 {
+ siblings[len(siblings)-1].Right = parent.Right
+ }
+ if len(parent.Children) != 0 {
+ parent.Children[len(siblings)-1].Right = parent.Right
+ }
+
+ //return siblings, nil
+ return parent.Children, nil
+}
+
+func (d *Darts) insert(siblings [](*LinkedListTrieNode)) (int, error) {
+ var begin int = 0
+ var pos int = max(int(siblings[0].Code)+1, d.nextCheckPos) - 1
+ var nonZeroNum int = 0
+ var first bool = false
+
+ if len(d.dat.Base) <= pos {
+ d.resize(pos + 1)
+ }
+
+ for {
+ next:
+ pos++
+
+ if len(d.dat.Base) <= pos {
+ d.resize(pos + 1)
+ }
+
+ if d.dat.Check[pos] > 0 {
+ nonZeroNum++
+ continue
+ } else if !first {
+ d.nextCheckPos = pos
+ first = true
+ }
+
+ begin = pos - int(siblings[0].Code)
+ if len(d.dat.Base) <= (begin + int(siblings[len(siblings)-1].Code)) {
+ d.resize(begin + int(siblings[len(siblings)-1].Code) + RESIZE_DELTA)
+ }
+
+ if d.used[begin] {
+ continue
+ }
+
+ for i := 1; i < len(siblings); i++ {
+ if 0 != d.dat.Check[begin+int(siblings[i].Code)] {
+ goto next
+ }
+ }
+ break
+
+ }
+
+ if float32(nonZeroNum)/float32(pos-d.nextCheckPos+1) >= 0.95 {
+ d.nextCheckPos = pos
+ }
+ d.used[begin] = true
+
+ for i := 0; i < len(siblings); i++ {
+ d.dat.Check[begin+int(siblings[i].Code)] = begin
+ }
+
+ for i := 0; i < len(siblings); i++ {
+ newSiblings, err := d.fetch(siblings[i])
+ if err != nil {
+ return -1, err
+ }
+
+ if len(newSiblings) == 0 {
+ d.dat.Base[begin+int(siblings[i].Code)] = -siblings[i].Left - 1
+ d.Output[begin+int(siblings[i].Code)] = siblings[i].SubKey
+ siblings[i].Base = END_NODE_BASE
+ siblings[i].Index = begin + int(siblings[i].Code)
+ } else {
+ h, err := d.insert(newSiblings)
+
+ if err != nil {
+ return -1, err
+ }
+ d.dat.Base[begin+int(siblings[i].Code)] = h
+ siblings[i].Index = begin + int(siblings[i].Code)
+ siblings[i].Base = h
+ }
+ }
+
+ return begin, nil
+}
+
+func (dat *DoubleArrayTrie) ExactMatchSearch(content []rune, nodePos int) bool {
+ b := dat.Base[nodePos]
+ var p int
+
+ for _, r := range content {
+ p = b + int(r) + 1
+ if b == dat.Check[p] {
+ b = dat.Base[p]
+ } else {
+ return false
+ }
+ }
+
+ p = b
+ n := dat.Base[p]
+ if b == dat.Check[p] && n < 0 {
+ return true
+ }
+
+ return false
+}
+
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
diff --git a/ahocorasick/go.mod b/ahocorasick/go.mod
new file mode 100644
index 00000000..e7f1d156
--- /dev/null
+++ b/ahocorasick/go.mod
@@ -0,0 +1,3 @@
+module friendnet.org/ahocorasick
+
+go 1.26.5
diff --git a/client/go.mod b/client/go.mod
index e515a8b3..e23c8e36 100644
--- a/client/go.mod
+++ b/client/go.mod
@@ -22,7 +22,6 @@ require (
require (
github.com/dustin/go-humanize v1.0.1 // indirect
- github.com/fsnotify/fsnotify v1.10.1 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
diff --git a/client/go.sum b/client/go.sum
index c6be9276..a0a4e794 100644
--- a/client/go.sum
+++ b/client/go.sum
@@ -4,8 +4,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
-github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx59Ho=
-github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
@@ -23,8 +21,9 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
-github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
+github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
+github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
+github.com/quic-go/quic-go v0.61.0 h1:ui88A53s8MSVYLC56en0KQ17HARk+9986Dn0SBfKNvA=
github.com/quic-go/quic-go v0.61.0/go.mod h1:9So2anK4Tp22URSQq00k+Vo2PNkle96ycDPDHL4s9vs=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -32,29 +31,24 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
-golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
-golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
+golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0=
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
-golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
-golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
-golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
-golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
+golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
+golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
+golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
-golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
-golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
-golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
-golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
+golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
-golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
-golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
+golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
+golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/common/go.mod b/common/go.mod
index ff11b687..efac5f4c 100644
--- a/common/go.mod
+++ b/common/go.mod
@@ -1,6 +1,6 @@
module friendnet.org/common
-go 1.26.2
+go 1.26.5
require (
connectrpc.com/connect v1.19.1
diff --git a/common/misc.go b/common/misc.go
index 7a909cd7..0505ae2f 100644
--- a/common/misc.go
+++ b/common/misc.go
@@ -3,6 +3,8 @@ package common
import (
"crypto/rand"
"encoding/base64"
+ "strings"
+ "unicode"
)
// StrPtrOr dereferences a string pointer or returns a default value if it is nil.
@@ -28,3 +30,8 @@ func RandomB64UrlStr(byteLen int) string {
_, _ = rand.Read(buf)
return base64.RawURLEncoding.EncodeToString(buf)
}
+
+// ToLowerUnicode lowercases a string, handling some weird unicode alphabets as well.
+func ToLowerUnicode(str string) string {
+ return strings.ToLowerSpecial(unicode.TurkishCase, str)
+}
diff --git a/go.work b/go.work
index dd06041a..907946a6 100644
--- a/go.work
+++ b/go.work
@@ -2,6 +2,7 @@ go 1.26.5
use (
./adminui
+ ./ahocorasick
./client
./common
./mkcert
diff --git a/go.work.sum b/go.work.sum
index 725d5d20..b335d6b9 100644
--- a/go.work.sum
+++ b/go.work.sum
@@ -1,3 +1,4 @@
+github.com/anyascii/go v0.3.3/go.mod h1:HDvbMmSpqJyIe+xtSkHmAYTjc8PzvO3l1Jmgx/IFUPs=
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
diff --git a/mkcert/go.mod b/mkcert/go.mod
index 5fb30231..22510c2b 100644
--- a/mkcert/go.mod
+++ b/mkcert/go.mod
@@ -1,6 +1,6 @@
module friendnet.org/mkcert
-go 1.26.2
+go 1.26.5
require (
friendnet.org/common v0.0.0
diff --git a/protocol/go.mod b/protocol/go.mod
index e3794e2a..995e7627 100644
--- a/protocol/go.mod
+++ b/protocol/go.mod
@@ -1,6 +1,6 @@
module friendnet.org/protocol
-go 1.26.2
+go 1.26.5
require (
connectrpc.com/connect v1.19.1
diff --git a/protocol/pb/serverrpc/v1/rpc.pb.go b/protocol/pb/serverrpc/v1/rpc.pb.go
index 8890971b..f144b04d 100644
--- a/protocol/pb/serverrpc/v1/rpc.pb.go
+++ b/protocol/pb/serverrpc/v1/rpc.pb.go
@@ -21,6 +21,62 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
+// BlacklistMatchMode are the possible modes for matching a word in a blacklist.
+type BlacklistMatchMode int32
+
+const (
+ BlacklistMatchMode_BLACKLIST_MATCH_MODE_UNSPECIFIED BlacklistMatchMode = 0
+ // Matches any substring of the word.
+ BlacklistMatchMode_BLACKLIST_MATCH_MODE_SUBSTRING BlacklistMatchMode = 1
+ // Matches only whole words that match.
+ BlacklistMatchMode_BLACKLIST_MATCH_MODE_WHOLE BlacklistMatchMode = 2
+ // Matches terms with regex.
+ BlacklistMatchMode_BLACKLIST_MATCH_MODE_REGEX BlacklistMatchMode = 3
+)
+
+// Enum value maps for BlacklistMatchMode.
+var (
+ BlacklistMatchMode_name = map[int32]string{
+ 0: "BLACKLIST_MATCH_MODE_UNSPECIFIED",
+ 1: "BLACKLIST_MATCH_MODE_SUBSTRING",
+ 2: "BLACKLIST_MATCH_MODE_WHOLE",
+ 3: "BLACKLIST_MATCH_MODE_REGEX",
+ }
+ BlacklistMatchMode_value = map[string]int32{
+ "BLACKLIST_MATCH_MODE_UNSPECIFIED": 0,
+ "BLACKLIST_MATCH_MODE_SUBSTRING": 1,
+ "BLACKLIST_MATCH_MODE_WHOLE": 2,
+ "BLACKLIST_MATCH_MODE_REGEX": 3,
+ }
+)
+
+func (x BlacklistMatchMode) Enum() *BlacklistMatchMode {
+ p := new(BlacklistMatchMode)
+ *p = x
+ return p
+}
+
+func (x BlacklistMatchMode) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (BlacklistMatchMode) Descriptor() protoreflect.EnumDescriptor {
+ return file_pb_serverrpc_v1_rpc_proto_enumTypes[0].Descriptor()
+}
+
+func (BlacklistMatchMode) Type() protoreflect.EnumType {
+ return &file_pb_serverrpc_v1_rpc_proto_enumTypes[0]
+}
+
+func (x BlacklistMatchMode) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use BlacklistMatchMode.Descriptor instead.
+func (BlacklistMatchMode) EnumDescriptor() ([]byte, []int) {
+ return file_pb_serverrpc_v1_rpc_proto_rawDescGZIP(), []int{0}
+}
+
// RoomInfo is information about a room.
type RoomInfo struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -1193,6 +1249,331 @@ func (x *UpdateAccountPasswordResponse) GetGeneratedPassword() string {
return ""
}
+// BlacklistPolicy is a word and the policy for how it will be enforced in a blacklist.
+type BlacklistPolicy struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // The keyword.
+ Keyword string `protobuf:"bytes,1,opt,name=keyword,proto3" json:"keyword,omitempty"`
+ // How the keyword will be matched.
+ Mode BlacklistMatchMode `protobuf:"varint,2,opt,name=mode,proto3,enum=pb.serverrpc.v1.BlacklistMatchMode" json:"mode,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *BlacklistPolicy) Reset() {
+ *x = BlacklistPolicy{}
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[25]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *BlacklistPolicy) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BlacklistPolicy) ProtoMessage() {}
+
+func (x *BlacklistPolicy) ProtoReflect() protoreflect.Message {
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[25]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use BlacklistPolicy.ProtoReflect.Descriptor instead.
+func (*BlacklistPolicy) Descriptor() ([]byte, []int) {
+ return file_pb_serverrpc_v1_rpc_proto_rawDescGZIP(), []int{25}
+}
+
+func (x *BlacklistPolicy) GetKeyword() string {
+ if x != nil {
+ return x.Keyword
+ }
+ return ""
+}
+
+func (x *BlacklistPolicy) GetMode() BlacklistMatchMode {
+ if x != nil {
+ return x.Mode
+ }
+ return BlacklistMatchMode_BLACKLIST_MATCH_MODE_UNSPECIFIED
+}
+
+type AddBlacklistPoliciesRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Room to enforce this policy. If null, then it is enforced serverwide.
+ Room *string `protobuf:"bytes,1,opt,name=room,proto3,oneof" json:"room,omitempty"`
+ // The policies to add.
+ Policies []*BlacklistPolicy `protobuf:"bytes,2,rep,name=policies,proto3" json:"policies,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AddBlacklistPoliciesRequest) Reset() {
+ *x = AddBlacklistPoliciesRequest{}
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[26]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AddBlacklistPoliciesRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AddBlacklistPoliciesRequest) ProtoMessage() {}
+
+func (x *AddBlacklistPoliciesRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[26]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AddBlacklistPoliciesRequest.ProtoReflect.Descriptor instead.
+func (*AddBlacklistPoliciesRequest) Descriptor() ([]byte, []int) {
+ return file_pb_serverrpc_v1_rpc_proto_rawDescGZIP(), []int{26}
+}
+
+func (x *AddBlacklistPoliciesRequest) GetRoom() string {
+ if x != nil && x.Room != nil {
+ return *x.Room
+ }
+ return ""
+}
+
+func (x *AddBlacklistPoliciesRequest) GetPolicies() []*BlacklistPolicy {
+ if x != nil {
+ return x.Policies
+ }
+ return nil
+}
+
+type AddBlacklistPoliciesResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *AddBlacklistPoliciesResponse) Reset() {
+ *x = AddBlacklistPoliciesResponse{}
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[27]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *AddBlacklistPoliciesResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AddBlacklistPoliciesResponse) ProtoMessage() {}
+
+func (x *AddBlacklistPoliciesResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[27]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use AddBlacklistPoliciesResponse.ProtoReflect.Descriptor instead.
+func (*AddBlacklistPoliciesResponse) Descriptor() ([]byte, []int) {
+ return file_pb_serverrpc_v1_rpc_proto_rawDescGZIP(), []int{27}
+}
+
+type RemoveBlacklistPoliciesRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Room in which this policy is enforced. If null, it is assumed to be serverwide.
+ Room *string `protobuf:"bytes,1,opt,name=room,proto3,oneof" json:"room,omitempty"`
+ // The policies to remove (identified by their keywords).
+ Policies []string `protobuf:"bytes,2,rep,name=policies,proto3" json:"policies,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RemoveBlacklistPoliciesRequest) Reset() {
+ *x = RemoveBlacklistPoliciesRequest{}
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[28]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RemoveBlacklistPoliciesRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RemoveBlacklistPoliciesRequest) ProtoMessage() {}
+
+func (x *RemoveBlacklistPoliciesRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[28]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RemoveBlacklistPoliciesRequest.ProtoReflect.Descriptor instead.
+func (*RemoveBlacklistPoliciesRequest) Descriptor() ([]byte, []int) {
+ return file_pb_serverrpc_v1_rpc_proto_rawDescGZIP(), []int{28}
+}
+
+func (x *RemoveBlacklistPoliciesRequest) GetRoom() string {
+ if x != nil && x.Room != nil {
+ return *x.Room
+ }
+ return ""
+}
+
+func (x *RemoveBlacklistPoliciesRequest) GetPolicies() []string {
+ if x != nil {
+ return x.Policies
+ }
+ return nil
+}
+
+type RemoveBlacklistPoliciesResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RemoveBlacklistPoliciesResponse) Reset() {
+ *x = RemoveBlacklistPoliciesResponse{}
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[29]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RemoveBlacklistPoliciesResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RemoveBlacklistPoliciesResponse) ProtoMessage() {}
+
+func (x *RemoveBlacklistPoliciesResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[29]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RemoveBlacklistPoliciesResponse.ProtoReflect.Descriptor instead.
+func (*RemoveBlacklistPoliciesResponse) Descriptor() ([]byte, []int) {
+ return file_pb_serverrpc_v1_rpc_proto_rawDescGZIP(), []int{29}
+}
+
+type ListBlacklistPoliciesRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // If null, it will return only global policies.
+ Room *string `protobuf:"bytes,1,opt,name=room,proto3,oneof" json:"room,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListBlacklistPoliciesRequest) Reset() {
+ *x = ListBlacklistPoliciesRequest{}
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[30]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListBlacklistPoliciesRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListBlacklistPoliciesRequest) ProtoMessage() {}
+
+func (x *ListBlacklistPoliciesRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[30]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListBlacklistPoliciesRequest.ProtoReflect.Descriptor instead.
+func (*ListBlacklistPoliciesRequest) Descriptor() ([]byte, []int) {
+ return file_pb_serverrpc_v1_rpc_proto_rawDescGZIP(), []int{30}
+}
+
+func (x *ListBlacklistPoliciesRequest) GetRoom() string {
+ if x != nil && x.Room != nil {
+ return *x.Room
+ }
+ return ""
+}
+
+type ListBlacklistPoliciesResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // The policies.
+ Policies []*BlacklistPolicy `protobuf:"bytes,1,rep,name=policies,proto3" json:"policies,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListBlacklistPoliciesResponse) Reset() {
+ *x = ListBlacklistPoliciesResponse{}
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[31]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListBlacklistPoliciesResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListBlacklistPoliciesResponse) ProtoMessage() {}
+
+func (x *ListBlacklistPoliciesResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[31]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListBlacklistPoliciesResponse.ProtoReflect.Descriptor instead.
+func (*ListBlacklistPoliciesResponse) Descriptor() ([]byte, []int) {
+ return file_pb_serverrpc_v1_rpc_proto_rawDescGZIP(), []int{31}
+}
+
+func (x *ListBlacklistPoliciesResponse) GetPolicies() []*BlacklistPolicy {
+ if x != nil {
+ return x.Policies
+ }
+ return nil
+}
+
type GetServerInfoResponse_Rpc struct {
state protoimpl.MessageState `protogen:"open.v1"`
// A list of all allowed methods on the RPC interface.
@@ -1206,7 +1587,7 @@ type GetServerInfoResponse_Rpc struct {
func (x *GetServerInfoResponse_Rpc) Reset() {
*x = GetServerInfoResponse_Rpc{}
- mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[25]
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1218,7 +1599,7 @@ func (x *GetServerInfoResponse_Rpc) String() string {
func (*GetServerInfoResponse_Rpc) ProtoMessage() {}
func (x *GetServerInfoResponse_Rpc) ProtoReflect() protoreflect.Message {
- mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[25]
+ mi := &file_pb_serverrpc_v1_rpc_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1312,7 +1693,30 @@ const file_pb_serverrpc_v1_rpc_proto_rawDesc = "" +
"\bpassword\x18\x03 \x01(\tR\bpassword\"j\n" +
"\x1dUpdateAccountPasswordResponse\x122\n" +
"\x12generated_password\x18\x01 \x01(\tH\x00R\x11generatedPassword\x88\x01\x01B\x15\n" +
- "\x13_generated_password2\xc4\b\n" +
+ "\x13_generated_password\"d\n" +
+ "\x0fBlacklistPolicy\x12\x18\n" +
+ "\akeyword\x18\x01 \x01(\tR\akeyword\x127\n" +
+ "\x04mode\x18\x02 \x01(\x0e2#.pb.serverrpc.v1.BlacklistMatchModeR\x04mode\"}\n" +
+ "\x1bAddBlacklistPoliciesRequest\x12\x17\n" +
+ "\x04room\x18\x01 \x01(\tH\x00R\x04room\x88\x01\x01\x12<\n" +
+ "\bpolicies\x18\x02 \x03(\v2 .pb.serverrpc.v1.BlacklistPolicyR\bpoliciesB\a\n" +
+ "\x05_room\"\x1e\n" +
+ "\x1cAddBlacklistPoliciesResponse\"^\n" +
+ "\x1eRemoveBlacklistPoliciesRequest\x12\x17\n" +
+ "\x04room\x18\x01 \x01(\tH\x00R\x04room\x88\x01\x01\x12\x1a\n" +
+ "\bpolicies\x18\x02 \x03(\tR\bpoliciesB\a\n" +
+ "\x05_room\"!\n" +
+ "\x1fRemoveBlacklistPoliciesResponse\"@\n" +
+ "\x1cListBlacklistPoliciesRequest\x12\x17\n" +
+ "\x04room\x18\x01 \x01(\tH\x00R\x04room\x88\x01\x01B\a\n" +
+ "\x05_room\"]\n" +
+ "\x1dListBlacklistPoliciesResponse\x12<\n" +
+ "\bpolicies\x18\x01 \x03(\v2 .pb.serverrpc.v1.BlacklistPolicyR\bpolicies*\x9e\x01\n" +
+ "\x12BlacklistMatchMode\x12$\n" +
+ " BLACKLIST_MATCH_MODE_UNSPECIFIED\x10\x00\x12\"\n" +
+ "\x1eBLACKLIST_MATCH_MODE_SUBSTRING\x10\x01\x12\x1e\n" +
+ "\x1aBLACKLIST_MATCH_MODE_WHOLE\x10\x02\x12\x1e\n" +
+ "\x1aBLACKLIST_MATCH_MODE_REGEX\x10\x032\xb5\v\n" +
"\x10ServerRpcService\x12`\n" +
"\rGetServerInfo\x12%.pb.serverrpc.v1.GetServerInfoRequest\x1a&.pb.serverrpc.v1.GetServerInfoResponse\"\x00\x12Q\n" +
"\bGetRooms\x12 .pb.serverrpc.v1.GetRoomsRequest\x1a!.pb.serverrpc.v1.GetRoomsResponse\"\x00\x12Z\n" +
@@ -1326,7 +1730,10 @@ const file_pb_serverrpc_v1_rpc_proto_rawDesc = "" +
"DeleteRoom\x12\".pb.serverrpc.v1.DeleteRoomRequest\x1a#.pb.serverrpc.v1.DeleteRoomResponse\"\x00\x12`\n" +
"\rCreateAccount\x12%.pb.serverrpc.v1.CreateAccountRequest\x1a&.pb.serverrpc.v1.CreateAccountResponse\"\x00\x12`\n" +
"\rDeleteAccount\x12%.pb.serverrpc.v1.DeleteAccountRequest\x1a&.pb.serverrpc.v1.DeleteAccountResponse\"\x00\x12x\n" +
- "\x15UpdateAccountPassword\x12-.pb.serverrpc.v1.UpdateAccountPasswordRequest\x1a..pb.serverrpc.v1.UpdateAccountPasswordResponse\"\x00B\xb1\x01\n" +
+ "\x15UpdateAccountPassword\x12-.pb.serverrpc.v1.UpdateAccountPasswordRequest\x1a..pb.serverrpc.v1.UpdateAccountPasswordResponse\"\x00\x12u\n" +
+ "\x14AddBlacklistPolicies\x12,.pb.serverrpc.v1.AddBlacklistPoliciesRequest\x1a-.pb.serverrpc.v1.AddBlacklistPoliciesResponse\"\x00\x12~\n" +
+ "\x17RemoveBlacklistPolicies\x12/.pb.serverrpc.v1.RemoveBlacklistPoliciesRequest\x1a0.pb.serverrpc.v1.RemoveBlacklistPoliciesResponse\"\x00\x12x\n" +
+ "\x15ListBlacklistPolicies\x12-.pb.serverrpc.v1.ListBlacklistPoliciesRequest\x1a..pb.serverrpc.v1.ListBlacklistPoliciesResponse\"\x00B\xb1\x01\n" +
"\x13com.pb.serverrpc.v1B\bRpcProtoP\x01Z2friendnet.org/protocol/pb/serverrpc/v1;serverrpcv1\xa2\x02\x03PSX\xaa\x02\x0fPb.Serverrpc.V1\xca\x02\x0fPb\\Serverrpc\\V1\xe2\x02\x1bPb\\Serverrpc\\V1\\GPBMetadata\xea\x02\x11Pb::Serverrpc::V1b\x06proto3"
var (
@@ -1341,71 +1748,89 @@ func file_pb_serverrpc_v1_rpc_proto_rawDescGZIP() []byte {
return file_pb_serverrpc_v1_rpc_proto_rawDescData
}
-var file_pb_serverrpc_v1_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
+var file_pb_serverrpc_v1_rpc_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_pb_serverrpc_v1_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 33)
var file_pb_serverrpc_v1_rpc_proto_goTypes = []any{
- (*RoomInfo)(nil), // 0: pb.serverrpc.v1.RoomInfo
- (*OnlineUserInfo)(nil), // 1: pb.serverrpc.v1.OnlineUserInfo
- (*AccountInfo)(nil), // 2: pb.serverrpc.v1.AccountInfo
- (*GetServerInfoRequest)(nil), // 3: pb.serverrpc.v1.GetServerInfoRequest
- (*GetServerInfoResponse)(nil), // 4: pb.serverrpc.v1.GetServerInfoResponse
- (*GetRoomsRequest)(nil), // 5: pb.serverrpc.v1.GetRoomsRequest
- (*GetRoomsResponse)(nil), // 6: pb.serverrpc.v1.GetRoomsResponse
- (*GetRoomInfoRequest)(nil), // 7: pb.serverrpc.v1.GetRoomInfoRequest
- (*GetRoomInfoResponse)(nil), // 8: pb.serverrpc.v1.GetRoomInfoResponse
- (*GetOnlineUsersRequest)(nil), // 9: pb.serverrpc.v1.GetOnlineUsersRequest
- (*GetOnlineUsersResponse)(nil), // 10: pb.serverrpc.v1.GetOnlineUsersResponse
- (*GetOnlineUserInfoRequest)(nil), // 11: pb.serverrpc.v1.GetOnlineUserInfoRequest
- (*GetOnlineUserInfoResponse)(nil), // 12: pb.serverrpc.v1.GetOnlineUserInfoResponse
- (*GetAccountsRequest)(nil), // 13: pb.serverrpc.v1.GetAccountsRequest
- (*GetAccountsResponse)(nil), // 14: pb.serverrpc.v1.GetAccountsResponse
- (*CreateRoomRequest)(nil), // 15: pb.serverrpc.v1.CreateRoomRequest
- (*CreateRoomResponse)(nil), // 16: pb.serverrpc.v1.CreateRoomResponse
- (*DeleteRoomRequest)(nil), // 17: pb.serverrpc.v1.DeleteRoomRequest
- (*DeleteRoomResponse)(nil), // 18: pb.serverrpc.v1.DeleteRoomResponse
- (*CreateAccountRequest)(nil), // 19: pb.serverrpc.v1.CreateAccountRequest
- (*CreateAccountResponse)(nil), // 20: pb.serverrpc.v1.CreateAccountResponse
- (*DeleteAccountRequest)(nil), // 21: pb.serverrpc.v1.DeleteAccountRequest
- (*DeleteAccountResponse)(nil), // 22: pb.serverrpc.v1.DeleteAccountResponse
- (*UpdateAccountPasswordRequest)(nil), // 23: pb.serverrpc.v1.UpdateAccountPasswordRequest
- (*UpdateAccountPasswordResponse)(nil), // 24: pb.serverrpc.v1.UpdateAccountPasswordResponse
- (*GetServerInfoResponse_Rpc)(nil), // 25: pb.serverrpc.v1.GetServerInfoResponse.Rpc
+ (BlacklistMatchMode)(0), // 0: pb.serverrpc.v1.BlacklistMatchMode
+ (*RoomInfo)(nil), // 1: pb.serverrpc.v1.RoomInfo
+ (*OnlineUserInfo)(nil), // 2: pb.serverrpc.v1.OnlineUserInfo
+ (*AccountInfo)(nil), // 3: pb.serverrpc.v1.AccountInfo
+ (*GetServerInfoRequest)(nil), // 4: pb.serverrpc.v1.GetServerInfoRequest
+ (*GetServerInfoResponse)(nil), // 5: pb.serverrpc.v1.GetServerInfoResponse
+ (*GetRoomsRequest)(nil), // 6: pb.serverrpc.v1.GetRoomsRequest
+ (*GetRoomsResponse)(nil), // 7: pb.serverrpc.v1.GetRoomsResponse
+ (*GetRoomInfoRequest)(nil), // 8: pb.serverrpc.v1.GetRoomInfoRequest
+ (*GetRoomInfoResponse)(nil), // 9: pb.serverrpc.v1.GetRoomInfoResponse
+ (*GetOnlineUsersRequest)(nil), // 10: pb.serverrpc.v1.GetOnlineUsersRequest
+ (*GetOnlineUsersResponse)(nil), // 11: pb.serverrpc.v1.GetOnlineUsersResponse
+ (*GetOnlineUserInfoRequest)(nil), // 12: pb.serverrpc.v1.GetOnlineUserInfoRequest
+ (*GetOnlineUserInfoResponse)(nil), // 13: pb.serverrpc.v1.GetOnlineUserInfoResponse
+ (*GetAccountsRequest)(nil), // 14: pb.serverrpc.v1.GetAccountsRequest
+ (*GetAccountsResponse)(nil), // 15: pb.serverrpc.v1.GetAccountsResponse
+ (*CreateRoomRequest)(nil), // 16: pb.serverrpc.v1.CreateRoomRequest
+ (*CreateRoomResponse)(nil), // 17: pb.serverrpc.v1.CreateRoomResponse
+ (*DeleteRoomRequest)(nil), // 18: pb.serverrpc.v1.DeleteRoomRequest
+ (*DeleteRoomResponse)(nil), // 19: pb.serverrpc.v1.DeleteRoomResponse
+ (*CreateAccountRequest)(nil), // 20: pb.serverrpc.v1.CreateAccountRequest
+ (*CreateAccountResponse)(nil), // 21: pb.serverrpc.v1.CreateAccountResponse
+ (*DeleteAccountRequest)(nil), // 22: pb.serverrpc.v1.DeleteAccountRequest
+ (*DeleteAccountResponse)(nil), // 23: pb.serverrpc.v1.DeleteAccountResponse
+ (*UpdateAccountPasswordRequest)(nil), // 24: pb.serverrpc.v1.UpdateAccountPasswordRequest
+ (*UpdateAccountPasswordResponse)(nil), // 25: pb.serverrpc.v1.UpdateAccountPasswordResponse
+ (*BlacklistPolicy)(nil), // 26: pb.serverrpc.v1.BlacklistPolicy
+ (*AddBlacklistPoliciesRequest)(nil), // 27: pb.serverrpc.v1.AddBlacklistPoliciesRequest
+ (*AddBlacklistPoliciesResponse)(nil), // 28: pb.serverrpc.v1.AddBlacklistPoliciesResponse
+ (*RemoveBlacklistPoliciesRequest)(nil), // 29: pb.serverrpc.v1.RemoveBlacklistPoliciesRequest
+ (*RemoveBlacklistPoliciesResponse)(nil), // 30: pb.serverrpc.v1.RemoveBlacklistPoliciesResponse
+ (*ListBlacklistPoliciesRequest)(nil), // 31: pb.serverrpc.v1.ListBlacklistPoliciesRequest
+ (*ListBlacklistPoliciesResponse)(nil), // 32: pb.serverrpc.v1.ListBlacklistPoliciesResponse
+ (*GetServerInfoResponse_Rpc)(nil), // 33: pb.serverrpc.v1.GetServerInfoResponse.Rpc
}
var file_pb_serverrpc_v1_rpc_proto_depIdxs = []int32{
- 25, // 0: pb.serverrpc.v1.GetServerInfoResponse.rpc:type_name -> pb.serverrpc.v1.GetServerInfoResponse.Rpc
- 0, // 1: pb.serverrpc.v1.GetRoomsResponse.rooms:type_name -> pb.serverrpc.v1.RoomInfo
- 0, // 2: pb.serverrpc.v1.GetRoomInfoResponse.room:type_name -> pb.serverrpc.v1.RoomInfo
- 1, // 3: pb.serverrpc.v1.GetOnlineUsersResponse.users:type_name -> pb.serverrpc.v1.OnlineUserInfo
- 1, // 4: pb.serverrpc.v1.GetOnlineUserInfoResponse.user:type_name -> pb.serverrpc.v1.OnlineUserInfo
- 2, // 5: pb.serverrpc.v1.GetAccountsResponse.accounts:type_name -> pb.serverrpc.v1.AccountInfo
- 0, // 6: pb.serverrpc.v1.CreateRoomResponse.room:type_name -> pb.serverrpc.v1.RoomInfo
- 2, // 7: pb.serverrpc.v1.CreateAccountResponse.account:type_name -> pb.serverrpc.v1.AccountInfo
- 3, // 8: pb.serverrpc.v1.ServerRpcService.GetServerInfo:input_type -> pb.serverrpc.v1.GetServerInfoRequest
- 5, // 9: pb.serverrpc.v1.ServerRpcService.GetRooms:input_type -> pb.serverrpc.v1.GetRoomsRequest
- 7, // 10: pb.serverrpc.v1.ServerRpcService.GetRoomInfo:input_type -> pb.serverrpc.v1.GetRoomInfoRequest
- 9, // 11: pb.serverrpc.v1.ServerRpcService.GetOnlineUsers:input_type -> pb.serverrpc.v1.GetOnlineUsersRequest
- 11, // 12: pb.serverrpc.v1.ServerRpcService.GetOnlineUserInfo:input_type -> pb.serverrpc.v1.GetOnlineUserInfoRequest
- 13, // 13: pb.serverrpc.v1.ServerRpcService.GetAccounts:input_type -> pb.serverrpc.v1.GetAccountsRequest
- 15, // 14: pb.serverrpc.v1.ServerRpcService.CreateRoom:input_type -> pb.serverrpc.v1.CreateRoomRequest
- 17, // 15: pb.serverrpc.v1.ServerRpcService.DeleteRoom:input_type -> pb.serverrpc.v1.DeleteRoomRequest
- 19, // 16: pb.serverrpc.v1.ServerRpcService.CreateAccount:input_type -> pb.serverrpc.v1.CreateAccountRequest
- 21, // 17: pb.serverrpc.v1.ServerRpcService.DeleteAccount:input_type -> pb.serverrpc.v1.DeleteAccountRequest
- 23, // 18: pb.serverrpc.v1.ServerRpcService.UpdateAccountPassword:input_type -> pb.serverrpc.v1.UpdateAccountPasswordRequest
- 4, // 19: pb.serverrpc.v1.ServerRpcService.GetServerInfo:output_type -> pb.serverrpc.v1.GetServerInfoResponse
- 6, // 20: pb.serverrpc.v1.ServerRpcService.GetRooms:output_type -> pb.serverrpc.v1.GetRoomsResponse
- 8, // 21: pb.serverrpc.v1.ServerRpcService.GetRoomInfo:output_type -> pb.serverrpc.v1.GetRoomInfoResponse
- 10, // 22: pb.serverrpc.v1.ServerRpcService.GetOnlineUsers:output_type -> pb.serverrpc.v1.GetOnlineUsersResponse
- 12, // 23: pb.serverrpc.v1.ServerRpcService.GetOnlineUserInfo:output_type -> pb.serverrpc.v1.GetOnlineUserInfoResponse
- 14, // 24: pb.serverrpc.v1.ServerRpcService.GetAccounts:output_type -> pb.serverrpc.v1.GetAccountsResponse
- 16, // 25: pb.serverrpc.v1.ServerRpcService.CreateRoom:output_type -> pb.serverrpc.v1.CreateRoomResponse
- 18, // 26: pb.serverrpc.v1.ServerRpcService.DeleteRoom:output_type -> pb.serverrpc.v1.DeleteRoomResponse
- 20, // 27: pb.serverrpc.v1.ServerRpcService.CreateAccount:output_type -> pb.serverrpc.v1.CreateAccountResponse
- 22, // 28: pb.serverrpc.v1.ServerRpcService.DeleteAccount:output_type -> pb.serverrpc.v1.DeleteAccountResponse
- 24, // 29: pb.serverrpc.v1.ServerRpcService.UpdateAccountPassword:output_type -> pb.serverrpc.v1.UpdateAccountPasswordResponse
- 19, // [19:30] is the sub-list for method output_type
- 8, // [8:19] is the sub-list for method input_type
- 8, // [8:8] is the sub-list for extension type_name
- 8, // [8:8] is the sub-list for extension extendee
- 0, // [0:8] is the sub-list for field type_name
+ 33, // 0: pb.serverrpc.v1.GetServerInfoResponse.rpc:type_name -> pb.serverrpc.v1.GetServerInfoResponse.Rpc
+ 1, // 1: pb.serverrpc.v1.GetRoomsResponse.rooms:type_name -> pb.serverrpc.v1.RoomInfo
+ 1, // 2: pb.serverrpc.v1.GetRoomInfoResponse.room:type_name -> pb.serverrpc.v1.RoomInfo
+ 2, // 3: pb.serverrpc.v1.GetOnlineUsersResponse.users:type_name -> pb.serverrpc.v1.OnlineUserInfo
+ 2, // 4: pb.serverrpc.v1.GetOnlineUserInfoResponse.user:type_name -> pb.serverrpc.v1.OnlineUserInfo
+ 3, // 5: pb.serverrpc.v1.GetAccountsResponse.accounts:type_name -> pb.serverrpc.v1.AccountInfo
+ 1, // 6: pb.serverrpc.v1.CreateRoomResponse.room:type_name -> pb.serverrpc.v1.RoomInfo
+ 3, // 7: pb.serverrpc.v1.CreateAccountResponse.account:type_name -> pb.serverrpc.v1.AccountInfo
+ 0, // 8: pb.serverrpc.v1.BlacklistPolicy.mode:type_name -> pb.serverrpc.v1.BlacklistMatchMode
+ 26, // 9: pb.serverrpc.v1.AddBlacklistPoliciesRequest.policies:type_name -> pb.serverrpc.v1.BlacklistPolicy
+ 26, // 10: pb.serverrpc.v1.ListBlacklistPoliciesResponse.policies:type_name -> pb.serverrpc.v1.BlacklistPolicy
+ 4, // 11: pb.serverrpc.v1.ServerRpcService.GetServerInfo:input_type -> pb.serverrpc.v1.GetServerInfoRequest
+ 6, // 12: pb.serverrpc.v1.ServerRpcService.GetRooms:input_type -> pb.serverrpc.v1.GetRoomsRequest
+ 8, // 13: pb.serverrpc.v1.ServerRpcService.GetRoomInfo:input_type -> pb.serverrpc.v1.GetRoomInfoRequest
+ 10, // 14: pb.serverrpc.v1.ServerRpcService.GetOnlineUsers:input_type -> pb.serverrpc.v1.GetOnlineUsersRequest
+ 12, // 15: pb.serverrpc.v1.ServerRpcService.GetOnlineUserInfo:input_type -> pb.serverrpc.v1.GetOnlineUserInfoRequest
+ 14, // 16: pb.serverrpc.v1.ServerRpcService.GetAccounts:input_type -> pb.serverrpc.v1.GetAccountsRequest
+ 16, // 17: pb.serverrpc.v1.ServerRpcService.CreateRoom:input_type -> pb.serverrpc.v1.CreateRoomRequest
+ 18, // 18: pb.serverrpc.v1.ServerRpcService.DeleteRoom:input_type -> pb.serverrpc.v1.DeleteRoomRequest
+ 20, // 19: pb.serverrpc.v1.ServerRpcService.CreateAccount:input_type -> pb.serverrpc.v1.CreateAccountRequest
+ 22, // 20: pb.serverrpc.v1.ServerRpcService.DeleteAccount:input_type -> pb.serverrpc.v1.DeleteAccountRequest
+ 24, // 21: pb.serverrpc.v1.ServerRpcService.UpdateAccountPassword:input_type -> pb.serverrpc.v1.UpdateAccountPasswordRequest
+ 27, // 22: pb.serverrpc.v1.ServerRpcService.AddBlacklistPolicies:input_type -> pb.serverrpc.v1.AddBlacklistPoliciesRequest
+ 29, // 23: pb.serverrpc.v1.ServerRpcService.RemoveBlacklistPolicies:input_type -> pb.serverrpc.v1.RemoveBlacklistPoliciesRequest
+ 31, // 24: pb.serverrpc.v1.ServerRpcService.ListBlacklistPolicies:input_type -> pb.serverrpc.v1.ListBlacklistPoliciesRequest
+ 5, // 25: pb.serverrpc.v1.ServerRpcService.GetServerInfo:output_type -> pb.serverrpc.v1.GetServerInfoResponse
+ 7, // 26: pb.serverrpc.v1.ServerRpcService.GetRooms:output_type -> pb.serverrpc.v1.GetRoomsResponse
+ 9, // 27: pb.serverrpc.v1.ServerRpcService.GetRoomInfo:output_type -> pb.serverrpc.v1.GetRoomInfoResponse
+ 11, // 28: pb.serverrpc.v1.ServerRpcService.GetOnlineUsers:output_type -> pb.serverrpc.v1.GetOnlineUsersResponse
+ 13, // 29: pb.serverrpc.v1.ServerRpcService.GetOnlineUserInfo:output_type -> pb.serverrpc.v1.GetOnlineUserInfoResponse
+ 15, // 30: pb.serverrpc.v1.ServerRpcService.GetAccounts:output_type -> pb.serverrpc.v1.GetAccountsResponse
+ 17, // 31: pb.serverrpc.v1.ServerRpcService.CreateRoom:output_type -> pb.serverrpc.v1.CreateRoomResponse
+ 19, // 32: pb.serverrpc.v1.ServerRpcService.DeleteRoom:output_type -> pb.serverrpc.v1.DeleteRoomResponse
+ 21, // 33: pb.serverrpc.v1.ServerRpcService.CreateAccount:output_type -> pb.serverrpc.v1.CreateAccountResponse
+ 23, // 34: pb.serverrpc.v1.ServerRpcService.DeleteAccount:output_type -> pb.serverrpc.v1.DeleteAccountResponse
+ 25, // 35: pb.serverrpc.v1.ServerRpcService.UpdateAccountPassword:output_type -> pb.serverrpc.v1.UpdateAccountPasswordResponse
+ 28, // 36: pb.serverrpc.v1.ServerRpcService.AddBlacklistPolicies:output_type -> pb.serverrpc.v1.AddBlacklistPoliciesResponse
+ 30, // 37: pb.serverrpc.v1.ServerRpcService.RemoveBlacklistPolicies:output_type -> pb.serverrpc.v1.RemoveBlacklistPoliciesResponse
+ 32, // 38: pb.serverrpc.v1.ServerRpcService.ListBlacklistPolicies:output_type -> pb.serverrpc.v1.ListBlacklistPoliciesResponse
+ 25, // [25:39] is the sub-list for method output_type
+ 11, // [11:25] is the sub-list for method input_type
+ 11, // [11:11] is the sub-list for extension type_name
+ 11, // [11:11] is the sub-list for extension extendee
+ 0, // [0:11] is the sub-list for field type_name
}
func init() { file_pb_serverrpc_v1_rpc_proto_init() }
@@ -1415,18 +1840,22 @@ func file_pb_serverrpc_v1_rpc_proto_init() {
}
file_pb_serverrpc_v1_rpc_proto_msgTypes[20].OneofWrappers = []any{}
file_pb_serverrpc_v1_rpc_proto_msgTypes[24].OneofWrappers = []any{}
+ file_pb_serverrpc_v1_rpc_proto_msgTypes[26].OneofWrappers = []any{}
+ file_pb_serverrpc_v1_rpc_proto_msgTypes[28].OneofWrappers = []any{}
+ file_pb_serverrpc_v1_rpc_proto_msgTypes[30].OneofWrappers = []any{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_pb_serverrpc_v1_rpc_proto_rawDesc), len(file_pb_serverrpc_v1_rpc_proto_rawDesc)),
- NumEnums: 0,
- NumMessages: 26,
+ NumEnums: 1,
+ NumMessages: 33,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_pb_serverrpc_v1_rpc_proto_goTypes,
DependencyIndexes: file_pb_serverrpc_v1_rpc_proto_depIdxs,
+ EnumInfos: file_pb_serverrpc_v1_rpc_proto_enumTypes,
MessageInfos: file_pb_serverrpc_v1_rpc_proto_msgTypes,
}.Build()
File_pb_serverrpc_v1_rpc_proto = out.File
diff --git a/protocol/pb/serverrpc/v1/rpc.proto b/protocol/pb/serverrpc/v1/rpc.proto
index 3c8b9331..1883942b 100644
--- a/protocol/pb/serverrpc/v1/rpc.proto
+++ b/protocol/pb/serverrpc/v1/rpc.proto
@@ -152,6 +152,60 @@ message UpdateAccountPasswordResponse {
optional string generated_password = 1;
}
+// BlacklistMatchMode are the possible modes for matching a word in a blacklist.
+enum BlacklistMatchMode {
+ BLACKLIST_MATCH_MODE_UNSPECIFIED = 0;
+
+ // Matches any substring of the word.
+ BLACKLIST_MATCH_MODE_SUBSTRING = 1;
+
+ // Matches only whole words that match.
+ BLACKLIST_MATCH_MODE_WHOLE = 2;
+
+ // Matches terms with regex.
+ BLACKLIST_MATCH_MODE_REGEX = 3;
+}
+
+// BlacklistPolicy is a word and the policy for how it will be enforced in a blacklist.
+message BlacklistPolicy {
+ // The keyword.
+ string keyword = 1;
+
+ // How the keyword will be matched.
+ BlacklistMatchMode mode = 2;
+}
+
+message AddBlacklistPoliciesRequest {
+ // Room to enforce this policy. If null, then it is enforced serverwide.
+ optional string room = 1;
+
+ // The policies to add.
+ repeated BlacklistPolicy policies = 2;
+}
+message AddBlacklistPoliciesResponse {
+
+}
+
+message RemoveBlacklistPoliciesRequest {
+ // Room in which this policy is enforced. If null, it is assumed to be serverwide.
+ optional string room = 1;
+
+ // The policies to remove (identified by their keywords).
+ repeated string policies = 2;
+}
+message RemoveBlacklistPoliciesResponse {
+
+}
+
+message ListBlacklistPoliciesRequest {
+ // If null, it will return only global policies.
+ optional string room = 1;
+}
+message ListBlacklistPoliciesResponse {
+ // The policies.
+ repeated BlacklistPolicy policies = 1;
+}
+
// ServerRpcService provides an RPC interface to a running FriendNet server.
// It can query state and perform administrative tasks.
//
@@ -207,4 +261,18 @@ service ServerRpcService {
// Returns status code NOT_FOUND if no such room exists.
// Returns status code NOT_FOUND if no such account exists.
rpc UpdateAccountPassword(UpdateAccountPasswordRequest) returns (UpdateAccountPasswordResponse) {}
+
+ // AddBlacklistPolicies adds one or more keywords that will be blacklisted from search queries and filenames.
+ // If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ // Returns status code INVALID_ARGUMENT if the keyword is empty or invalid for the specified match mode.
+ rpc AddBlacklistPolicies(AddBlacklistPoliciesRequest) returns (AddBlacklistPoliciesResponse) {}
+
+ // RemoveBlacklistPolicies removes one or more keyword policies from the blacklists.
+ // If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ rpc RemoveBlacklistPolicies(RemoveBlacklistPoliciesRequest) returns (RemoveBlacklistPoliciesResponse) {}
+
+ // ListBlacklistPolicies returns a list of currently blacklisted keywords for a room.
+ // If the room is not set, the list returned will contain only serverwide policies.
+ // Returns status code NOT_FOUND if the room does not exist.
+ rpc ListBlacklistPolicies(ListBlacklistPoliciesRequest) returns (ListBlacklistPoliciesResponse) {}
}
diff --git a/protocol/pb/serverrpc/v1/serverrpcv1connect/rpc.connect.go b/protocol/pb/serverrpc/v1/serverrpcv1connect/rpc.connect.go
index 6853c802..5c7d0ef6 100644
--- a/protocol/pb/serverrpc/v1/serverrpcv1connect/rpc.connect.go
+++ b/protocol/pb/serverrpc/v1/serverrpcv1connect/rpc.connect.go
@@ -66,6 +66,15 @@ const (
// ServerRpcServiceUpdateAccountPasswordProcedure is the fully-qualified name of the
// ServerRpcService's UpdateAccountPassword RPC.
ServerRpcServiceUpdateAccountPasswordProcedure = "/pb.serverrpc.v1.ServerRpcService/UpdateAccountPassword"
+ // ServerRpcServiceAddBlacklistPoliciesProcedure is the fully-qualified name of the
+ // ServerRpcService's AddBlacklistPolicies RPC.
+ ServerRpcServiceAddBlacklistPoliciesProcedure = "/pb.serverrpc.v1.ServerRpcService/AddBlacklistPolicies"
+ // ServerRpcServiceRemoveBlacklistPoliciesProcedure is the fully-qualified name of the
+ // ServerRpcService's RemoveBlacklistPolicies RPC.
+ ServerRpcServiceRemoveBlacklistPoliciesProcedure = "/pb.serverrpc.v1.ServerRpcService/RemoveBlacklistPolicies"
+ // ServerRpcServiceListBlacklistPoliciesProcedure is the fully-qualified name of the
+ // ServerRpcService's ListBlacklistPolicies RPC.
+ ServerRpcServiceListBlacklistPoliciesProcedure = "/pb.serverrpc.v1.ServerRpcService/ListBlacklistPolicies"
)
// ServerRpcServiceClient is a client for the pb.serverrpc.v1.ServerRpcService service.
@@ -109,6 +118,17 @@ type ServerRpcServiceClient interface {
// Returns status code NOT_FOUND if no such room exists.
// Returns status code NOT_FOUND if no such account exists.
UpdateAccountPassword(context.Context, *v1.UpdateAccountPasswordRequest) (*v1.UpdateAccountPasswordResponse, error)
+ // AddBlacklistPolicies adds one or more keywords that will be blacklisted from search queries and filenames.
+ // If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ // Returns status code INVALID_ARGUMENT if the keyword is empty or invalid for the specified match mode.
+ AddBlacklistPolicies(context.Context, *v1.AddBlacklistPoliciesRequest) (*v1.AddBlacklistPoliciesResponse, error)
+ // RemoveBlacklistPolicies removes one or more keyword policies from the blacklists.
+ // If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ RemoveBlacklistPolicies(context.Context, *v1.RemoveBlacklistPoliciesRequest) (*v1.RemoveBlacklistPoliciesResponse, error)
+ // ListBlacklistPolicies returns a list of currently blacklisted keywords for a room.
+ // If the room is not set, the list returned will contain only serverwide policies.
+ // Returns status code NOT_FOUND if the room does not exist.
+ ListBlacklistPolicies(context.Context, *v1.ListBlacklistPoliciesRequest) (*v1.ListBlacklistPoliciesResponse, error)
}
// NewServerRpcServiceClient constructs a client for the pb.serverrpc.v1.ServerRpcService service.
@@ -188,22 +208,43 @@ func NewServerRpcServiceClient(httpClient connect.HTTPClient, baseURL string, op
connect.WithSchema(serverRpcServiceMethods.ByName("UpdateAccountPassword")),
connect.WithClientOptions(opts...),
),
+ addBlacklistPolicies: connect.NewClient[v1.AddBlacklistPoliciesRequest, v1.AddBlacklistPoliciesResponse](
+ httpClient,
+ baseURL+ServerRpcServiceAddBlacklistPoliciesProcedure,
+ connect.WithSchema(serverRpcServiceMethods.ByName("AddBlacklistPolicies")),
+ connect.WithClientOptions(opts...),
+ ),
+ removeBlacklistPolicies: connect.NewClient[v1.RemoveBlacklistPoliciesRequest, v1.RemoveBlacklistPoliciesResponse](
+ httpClient,
+ baseURL+ServerRpcServiceRemoveBlacklistPoliciesProcedure,
+ connect.WithSchema(serverRpcServiceMethods.ByName("RemoveBlacklistPolicies")),
+ connect.WithClientOptions(opts...),
+ ),
+ listBlacklistPolicies: connect.NewClient[v1.ListBlacklistPoliciesRequest, v1.ListBlacklistPoliciesResponse](
+ httpClient,
+ baseURL+ServerRpcServiceListBlacklistPoliciesProcedure,
+ connect.WithSchema(serverRpcServiceMethods.ByName("ListBlacklistPolicies")),
+ connect.WithClientOptions(opts...),
+ ),
}
}
// serverRpcServiceClient implements ServerRpcServiceClient.
type serverRpcServiceClient struct {
- getServerInfo *connect.Client[v1.GetServerInfoRequest, v1.GetServerInfoResponse]
- getRooms *connect.Client[v1.GetRoomsRequest, v1.GetRoomsResponse]
- getRoomInfo *connect.Client[v1.GetRoomInfoRequest, v1.GetRoomInfoResponse]
- getOnlineUsers *connect.Client[v1.GetOnlineUsersRequest, v1.GetOnlineUsersResponse]
- getOnlineUserInfo *connect.Client[v1.GetOnlineUserInfoRequest, v1.GetOnlineUserInfoResponse]
- getAccounts *connect.Client[v1.GetAccountsRequest, v1.GetAccountsResponse]
- createRoom *connect.Client[v1.CreateRoomRequest, v1.CreateRoomResponse]
- deleteRoom *connect.Client[v1.DeleteRoomRequest, v1.DeleteRoomResponse]
- createAccount *connect.Client[v1.CreateAccountRequest, v1.CreateAccountResponse]
- deleteAccount *connect.Client[v1.DeleteAccountRequest, v1.DeleteAccountResponse]
- updateAccountPassword *connect.Client[v1.UpdateAccountPasswordRequest, v1.UpdateAccountPasswordResponse]
+ getServerInfo *connect.Client[v1.GetServerInfoRequest, v1.GetServerInfoResponse]
+ getRooms *connect.Client[v1.GetRoomsRequest, v1.GetRoomsResponse]
+ getRoomInfo *connect.Client[v1.GetRoomInfoRequest, v1.GetRoomInfoResponse]
+ getOnlineUsers *connect.Client[v1.GetOnlineUsersRequest, v1.GetOnlineUsersResponse]
+ getOnlineUserInfo *connect.Client[v1.GetOnlineUserInfoRequest, v1.GetOnlineUserInfoResponse]
+ getAccounts *connect.Client[v1.GetAccountsRequest, v1.GetAccountsResponse]
+ createRoom *connect.Client[v1.CreateRoomRequest, v1.CreateRoomResponse]
+ deleteRoom *connect.Client[v1.DeleteRoomRequest, v1.DeleteRoomResponse]
+ createAccount *connect.Client[v1.CreateAccountRequest, v1.CreateAccountResponse]
+ deleteAccount *connect.Client[v1.DeleteAccountRequest, v1.DeleteAccountResponse]
+ updateAccountPassword *connect.Client[v1.UpdateAccountPasswordRequest, v1.UpdateAccountPasswordResponse]
+ addBlacklistPolicies *connect.Client[v1.AddBlacklistPoliciesRequest, v1.AddBlacklistPoliciesResponse]
+ removeBlacklistPolicies *connect.Client[v1.RemoveBlacklistPoliciesRequest, v1.RemoveBlacklistPoliciesResponse]
+ listBlacklistPolicies *connect.Client[v1.ListBlacklistPoliciesRequest, v1.ListBlacklistPoliciesResponse]
}
// GetServerInfo calls pb.serverrpc.v1.ServerRpcService.GetServerInfo.
@@ -301,6 +342,33 @@ func (c *serverRpcServiceClient) UpdateAccountPassword(ctx context.Context, req
return nil, err
}
+// AddBlacklistPolicies calls pb.serverrpc.v1.ServerRpcService.AddBlacklistPolicies.
+func (c *serverRpcServiceClient) AddBlacklistPolicies(ctx context.Context, req *v1.AddBlacklistPoliciesRequest) (*v1.AddBlacklistPoliciesResponse, error) {
+ response, err := c.addBlacklistPolicies.CallUnary(ctx, connect.NewRequest(req))
+ if response != nil {
+ return response.Msg, err
+ }
+ return nil, err
+}
+
+// RemoveBlacklistPolicies calls pb.serverrpc.v1.ServerRpcService.RemoveBlacklistPolicies.
+func (c *serverRpcServiceClient) RemoveBlacklistPolicies(ctx context.Context, req *v1.RemoveBlacklistPoliciesRequest) (*v1.RemoveBlacklistPoliciesResponse, error) {
+ response, err := c.removeBlacklistPolicies.CallUnary(ctx, connect.NewRequest(req))
+ if response != nil {
+ return response.Msg, err
+ }
+ return nil, err
+}
+
+// ListBlacklistPolicies calls pb.serverrpc.v1.ServerRpcService.ListBlacklistPolicies.
+func (c *serverRpcServiceClient) ListBlacklistPolicies(ctx context.Context, req *v1.ListBlacklistPoliciesRequest) (*v1.ListBlacklistPoliciesResponse, error) {
+ response, err := c.listBlacklistPolicies.CallUnary(ctx, connect.NewRequest(req))
+ if response != nil {
+ return response.Msg, err
+ }
+ return nil, err
+}
+
// ServerRpcServiceHandler is an implementation of the pb.serverrpc.v1.ServerRpcService service.
type ServerRpcServiceHandler interface {
// GetServerInfo returns information about the server.
@@ -342,6 +410,17 @@ type ServerRpcServiceHandler interface {
// Returns status code NOT_FOUND if no such room exists.
// Returns status code NOT_FOUND if no such account exists.
UpdateAccountPassword(context.Context, *v1.UpdateAccountPasswordRequest) (*v1.UpdateAccountPasswordResponse, error)
+ // AddBlacklistPolicies adds one or more keywords that will be blacklisted from search queries and filenames.
+ // If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ // Returns status code INVALID_ARGUMENT if the keyword is empty or invalid for the specified match mode.
+ AddBlacklistPolicies(context.Context, *v1.AddBlacklistPoliciesRequest) (*v1.AddBlacklistPoliciesResponse, error)
+ // RemoveBlacklistPolicies removes one or more keyword policies from the blacklists.
+ // If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ RemoveBlacklistPolicies(context.Context, *v1.RemoveBlacklistPoliciesRequest) (*v1.RemoveBlacklistPoliciesResponse, error)
+ // ListBlacklistPolicies returns a list of currently blacklisted keywords for a room.
+ // If the room is not set, the list returned will contain only serverwide policies.
+ // Returns status code NOT_FOUND if the room does not exist.
+ ListBlacklistPolicies(context.Context, *v1.ListBlacklistPoliciesRequest) (*v1.ListBlacklistPoliciesResponse, error)
}
// NewServerRpcServiceHandler builds an HTTP handler from the service implementation. It returns the
@@ -417,6 +496,24 @@ func NewServerRpcServiceHandler(svc ServerRpcServiceHandler, opts ...connect.Han
connect.WithSchema(serverRpcServiceMethods.ByName("UpdateAccountPassword")),
connect.WithHandlerOptions(opts...),
)
+ serverRpcServiceAddBlacklistPoliciesHandler := connect.NewUnaryHandlerSimple(
+ ServerRpcServiceAddBlacklistPoliciesProcedure,
+ svc.AddBlacklistPolicies,
+ connect.WithSchema(serverRpcServiceMethods.ByName("AddBlacklistPolicies")),
+ connect.WithHandlerOptions(opts...),
+ )
+ serverRpcServiceRemoveBlacklistPoliciesHandler := connect.NewUnaryHandlerSimple(
+ ServerRpcServiceRemoveBlacklistPoliciesProcedure,
+ svc.RemoveBlacklistPolicies,
+ connect.WithSchema(serverRpcServiceMethods.ByName("RemoveBlacklistPolicies")),
+ connect.WithHandlerOptions(opts...),
+ )
+ serverRpcServiceListBlacklistPoliciesHandler := connect.NewUnaryHandlerSimple(
+ ServerRpcServiceListBlacklistPoliciesProcedure,
+ svc.ListBlacklistPolicies,
+ connect.WithSchema(serverRpcServiceMethods.ByName("ListBlacklistPolicies")),
+ connect.WithHandlerOptions(opts...),
+ )
return "/pb.serverrpc.v1.ServerRpcService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case ServerRpcServiceGetServerInfoProcedure:
@@ -441,6 +538,12 @@ func NewServerRpcServiceHandler(svc ServerRpcServiceHandler, opts ...connect.Han
serverRpcServiceDeleteAccountHandler.ServeHTTP(w, r)
case ServerRpcServiceUpdateAccountPasswordProcedure:
serverRpcServiceUpdateAccountPasswordHandler.ServeHTTP(w, r)
+ case ServerRpcServiceAddBlacklistPoliciesProcedure:
+ serverRpcServiceAddBlacklistPoliciesHandler.ServeHTTP(w, r)
+ case ServerRpcServiceRemoveBlacklistPoliciesProcedure:
+ serverRpcServiceRemoveBlacklistPoliciesHandler.ServeHTTP(w, r)
+ case ServerRpcServiceListBlacklistPoliciesProcedure:
+ serverRpcServiceListBlacklistPoliciesHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
@@ -493,3 +596,15 @@ func (UnimplementedServerRpcServiceHandler) DeleteAccount(context.Context, *v1.D
func (UnimplementedServerRpcServiceHandler) UpdateAccountPassword(context.Context, *v1.UpdateAccountPasswordRequest) (*v1.UpdateAccountPasswordResponse, error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("pb.serverrpc.v1.ServerRpcService.UpdateAccountPassword is not implemented"))
}
+
+func (UnimplementedServerRpcServiceHandler) AddBlacklistPolicies(context.Context, *v1.AddBlacklistPoliciesRequest) (*v1.AddBlacklistPoliciesResponse, error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("pb.serverrpc.v1.ServerRpcService.AddBlacklistPolicies is not implemented"))
+}
+
+func (UnimplementedServerRpcServiceHandler) RemoveBlacklistPolicies(context.Context, *v1.RemoveBlacklistPoliciesRequest) (*v1.RemoveBlacklistPoliciesResponse, error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("pb.serverrpc.v1.ServerRpcService.RemoveBlacklistPolicies is not implemented"))
+}
+
+func (UnimplementedServerRpcServiceHandler) ListBlacklistPolicies(context.Context, *v1.ListBlacklistPoliciesRequest) (*v1.ListBlacklistPoliciesResponse, error) {
+ return nil, connect.NewError(connect.CodeUnimplemented, errors.New("pb.serverrpc.v1.ServerRpcService.ListBlacklistPolicies is not implemented"))
+}
diff --git a/rpcclient/cli.go b/rpcclient/cli.go
index 65850fef..5df7fe9c 100644
--- a/rpcclient/cli.go
+++ b/rpcclient/cli.go
@@ -4,17 +4,38 @@ import (
"context"
"errors"
"fmt"
+ "math"
"net/http"
"os"
"slices"
"strings"
"connectrpc.com/connect"
+ "friendnet.org/common"
v1 "friendnet.org/protocol/pb/serverrpc/v1"
"friendnet.org/protocol/pb/serverrpc/v1/serverrpcv1connect"
"github.com/chzyer/readline"
)
+var matchModeToName = map[v1.BlacklistMatchMode]string{
+ v1.BlacklistMatchMode_BLACKLIST_MATCH_MODE_SUBSTRING: "substring",
+ v1.BlacklistMatchMode_BLACKLIST_MATCH_MODE_WHOLE: "whole",
+}
+var nameToMatchMode = func() map[string]v1.BlacklistMatchMode {
+ res := make(map[string]v1.BlacklistMatchMode, len(matchModeToName))
+ for mode, name := range matchModeToName {
+ res[name] = mode
+ }
+ return res
+}()
+var matchModeNames = func() []string {
+ names := make([]string, 0, len(nameToMatchMode))
+ for _, name := range matchModeToName {
+ names = append(names, name)
+ }
+ return names
+}()
+
// Opt is a function that configures a CLI.
type Opt func(*Cli)
@@ -36,7 +57,7 @@ func WithWelcomeMsg(msg string) Opt {
type Cmd struct {
Name string
Usage string
- Handler func(ctx context.Context, cli *Cli, args []string) error
+ Handler func(ctx context.Context, cmd Cmd, args []string) error
}
// Cli is a command-line interface for the server RPC service.
@@ -64,95 +85,94 @@ func NewCli(client serverrpcv1connect.ServerRpcServiceClient, opts ...Opt) *Cli
cli.commands = []Cmd{
{
- Name: "help",
- Usage: "help [command]",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdHelp(ctx, args)
- },
+ Name: "help",
+ Usage: "[command]",
+ Handler: cli.cmdHelp,
+ },
+ {
+ Name: "exit",
+ Usage: "exit",
+ Handler: cli.cmdExit,
},
{
- Name: "exit",
- Usage: "exit",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdExit(ctx, args)
- },
+ Name: "getserverinfo",
+ Usage: "",
+ Handler: cli.cmdGetServerInfo,
},
{
- Name: "getserverinfo",
- Usage: "getserverinfo",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdGetServerInfo(ctx, args)
- },
+ Name: "getrooms",
+ Usage: "",
+ Handler: cli.cmdGetRooms,
},
{
- Name: "getrooms",
- Usage: "getrooms",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdGetRooms(ctx, args)
- },
+ Name: "getroominfo",
+ Usage: "",
+ Handler: cli.cmdGetRoomInfo,
},
{
- Name: "getroominfo",
- Usage: "getroominfo ",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdGetRoomInfo(ctx, args)
- },
+ Name: "getonlineusers",
+ Usage: "",
+ Handler: cli.cmdGetOnlineUsers,
},
{
- Name: "getonlineusers",
- Usage: "getonlineusers ",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdGetOnlineUsers(ctx, args)
- },
+ Name: "getonlineuserinfo",
+ Usage: " ",
+ Handler: cli.cmdGetOnlineUserInfo,
},
{
- Name: "getonlineuserinfo",
- Usage: "getonlineuserinfo ",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdGetOnlineUserInfo(ctx, args)
- },
+ Name: "getaccounts",
+ Usage: "",
+ Handler: cli.cmdGetAccounts,
},
{
- Name: "getaccounts",
- Usage: "getaccounts ",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdGetAccounts(ctx, args)
- },
+ Name: "createroom",
+ Usage: "",
+ Handler: cli.cmdCreateRoom,
},
{
- Name: "createroom",
- Usage: "createroom ",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdCreateRoom(ctx, args)
- },
+ Name: "deleteroom",
+ Usage: "",
+ Handler: cli.cmdDeleteRoom,
},
{
- Name: "deleteroom",
- Usage: "deleteroom ",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdDeleteRoom(ctx, args)
- },
+ Name: "createaccount",
+ Usage: " [password]",
+ Handler: cli.cmdCreateAccount,
},
{
- Name: "createaccount",
- Usage: "createaccount [password]",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdCreateAccount(ctx, args)
- },
+ Name: "deleteaccount",
+ Usage: " ",
+ Handler: cli.cmdDeleteAccount,
},
{
- Name: "deleteaccount",
- Usage: "deleteaccount ",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdDeleteAccount(ctx, args)
- },
+ Name: "updateaccountpassword",
+ Usage: " [password]",
+ Handler: cli.cmdUpdateAccountPassword,
},
{
- Name: "updateaccountpassword",
- Usage: "updateaccountpassword [password]",
- Handler: func(ctx context.Context, cli *Cli, args []string) error {
- return cli.cmdUpdateAccountPassword(ctx, args)
- },
+ Name: "addglobalblacklistpolicies",
+ Usage: fmt.Sprintf("<%s> ", strings.Join(matchModeNames, "|")),
+ Handler: cli.cmdAddGlobalBlacklistPolicies,
+ },
+ {
+ Name: "addroomblacklistpolicies",
+ Usage: fmt.Sprintf(" <%s> ", strings.Join(matchModeNames, "|")),
+ Handler: cli.cmdAddRoomBlacklistPolicies,
+ },
+ {
+ Name: "removeglobalblacklistpolicies",
+ Usage: "removeglobalblacklistpolicies ",
+ Handler: cli.cmdRemoveGlobalBlacklistPolicies,
+ },
+ {
+ Name: "removeroomblacklistpolicies",
+ Usage: " ",
+ Handler: cli.cmdRemoveRoomBlacklistPolicies,
+ },
+ {
+ Name: "listblacklistpolicies",
+ Usage: "[room]",
+ Handler: cli.cmdListBlacklistPolicies,
},
}
return cli
@@ -181,7 +201,7 @@ func (c *Cli) Do(cmdStr string) error {
name := parts[0]
for _, cmd := range c.commands {
if cmd.Name == name {
- return cmd.Handler(c.mkCtx(), c, parts[1:])
+ return cmd.Handler(c.mkCtx(), cmd, parts[1:])
}
}
@@ -190,7 +210,7 @@ func (c *Cli) Do(cmdStr string) error {
return nil
}
-func (c *Cli) cmdHelp(_ context.Context, args []string) error {
+func (c *Cli) cmdHelp(_ context.Context, _ Cmd, args []string) error {
if len(args) > 1 {
return fmt.Errorf("usage: help [command]")
}
@@ -219,11 +239,11 @@ func (c *Cli) cmdHelp(_ context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdExit(_ context.Context, _ []string) error {
+func (c *Cli) cmdExit(_ context.Context, _ Cmd, _ []string) error {
return errStop
}
-func (c *Cli) cmdGetServerInfo(ctx context.Context, _ []string) error {
+func (c *Cli) cmdGetServerInfo(ctx context.Context, _ Cmd, _ []string) error {
resp, err := c.client.GetServerInfo(ctx, &v1.GetServerInfoRequest{})
if err != nil {
return err
@@ -244,8 +264,8 @@ func (c *Cli) cmdGetServerInfo(ctx context.Context, _ []string) error {
return nil
}
-func (c *Cli) cmdGetRooms(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 0, 0, "getrooms"); err != nil {
+func (c *Cli) cmdGetRooms(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 0, 0, cmd); err != nil {
return err
}
@@ -268,8 +288,8 @@ func (c *Cli) cmdGetRooms(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdGetRoomInfo(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 1, 1, "getroominfo "); err != nil {
+func (c *Cli) cmdGetRoomInfo(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 1, 1, cmd); err != nil {
return err
}
@@ -289,8 +309,8 @@ func (c *Cli) cmdGetRoomInfo(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdGetOnlineUsers(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 1, 1, "getonlineusers "); err != nil {
+func (c *Cli) cmdGetOnlineUsers(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 1, 1, cmd); err != nil {
return err
}
@@ -321,8 +341,8 @@ func (c *Cli) cmdGetOnlineUsers(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdGetOnlineUserInfo(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 2, 2, "getonlineuserinfo "); err != nil {
+func (c *Cli) cmdGetOnlineUserInfo(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 2, 2, cmd); err != nil {
return err
}
@@ -343,8 +363,8 @@ func (c *Cli) cmdGetOnlineUserInfo(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdGetAccounts(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 1, 1, "getaccounts "); err != nil {
+func (c *Cli) cmdGetAccounts(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 1, 1, cmd); err != nil {
return err
}
@@ -369,8 +389,8 @@ func (c *Cli) cmdGetAccounts(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdCreateRoom(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 1, 1, "createroom "); err != nil {
+func (c *Cli) cmdCreateRoom(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 1, 1, cmd); err != nil {
return err
}
@@ -390,8 +410,8 @@ func (c *Cli) cmdCreateRoom(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdDeleteRoom(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 1, 1, "deleteroom "); err != nil {
+func (c *Cli) cmdDeleteRoom(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 1, 1, cmd); err != nil {
return err
}
@@ -406,8 +426,8 @@ func (c *Cli) cmdDeleteRoom(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdCreateAccount(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 2, 3, "createaccount [password]"); err != nil {
+func (c *Cli) cmdCreateAccount(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 2, 3, cmd); err != nil {
return err
}
@@ -433,8 +453,8 @@ func (c *Cli) cmdCreateAccount(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdDeleteAccount(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 2, 2, "deleteaccount "); err != nil {
+func (c *Cli) cmdDeleteAccount(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 2, 2, cmd); err != nil {
return err
}
@@ -450,8 +470,8 @@ func (c *Cli) cmdDeleteAccount(ctx context.Context, args []string) error {
return nil
}
-func (c *Cli) cmdUpdateAccountPassword(ctx context.Context, args []string) error {
- if err := validateArgCount(args, 2, 3, "updateaccountpassword [password]"); err != nil {
+func (c *Cli) cmdUpdateAccountPassword(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 2, 3, cmd); err != nil {
return err
}
@@ -481,13 +501,133 @@ func (c *Cli) cmdUpdateAccountPassword(ctx context.Context, args []string) error
return nil
}
-func validateArgCount(args []string, min int, max int, usage string) error {
- if len(args) < min {
- return fmt.Errorf("usage: %s", usage)
+const cmdBlacklistModeSubstring = "substring"
+const cmdBlacklistModeWhole = "whole"
+
+func (c *Cli) addBlacklistPolicies(ctx context.Context, room string, modeName string, keywords []string) error {
+ mode, ok := nameToMatchMode[modeName]
+ if !ok {
+ return fmt.Errorf("unknown match mode %q", modeName)
+ }
+
+ policiesEmpty := make([]v1.BlacklistPolicy, len(keywords))
+ policies := make([]*v1.BlacklistPolicy, len(keywords))
+ for i, keyword := range keywords {
+ policy := &policiesEmpty[i]
+ policy.Keyword = keyword
+ policy.Mode = mode
+ policies[i] = policy
+ }
+
+ _, err := c.client.AddBlacklistPolicies(ctx, &v1.AddBlacklistPoliciesRequest{
+ Room: common.StrOrNil(room),
+ Policies: policies,
+ })
+ return err
+}
+
+func (c *Cli) cmdAddGlobalBlacklistPolicies(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 2, math.MaxInt64, cmd); err != nil {
+ return err
+ }
+
+ return c.addBlacklistPolicies(ctx, "", args[0], args[1:])
+}
+
+func (c *Cli) cmdRemoveGlobalBlacklistPolicies(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 1, math.MaxInt64, cmd); err != nil {
+ return err
+ }
+
+ var room *string
+ if len(args) == 2 {
+ room = &args[1]
+ }
+
+ _, err := c.client.RemoveBlacklistPolicies(ctx, &v1.RemoveBlacklistPoliciesRequest{
+ Room: room,
+ Policies: args,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Cli) cmdAddRoomBlacklistPolicies(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 3, math.MaxInt64, cmd); err != nil {
+ return err
+ }
+
+ return c.addBlacklistPolicies(ctx, args[0], args[1], args[2:])
+}
+
+func (c *Cli) cmdRemoveRoomBlacklistPolicies(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 1, 2, cmd); err != nil {
+ return err
+ }
+
+ var room *string
+ if len(args) == 2 {
+ room = &args[1]
+ }
+
+ _, err := c.client.RemoveBlacklistPolicies(ctx, &v1.RemoveBlacklistPoliciesRequest{
+ Room: room,
+ Policies: args,
+ })
+
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Cli) cmdListBlacklistPolicies(ctx context.Context, cmd Cmd, args []string) error {
+ if err := validateArgCount(args, 0, 1, cmd); err != nil {
+ return err
}
- if max >= 0 && len(args) > max {
- return fmt.Errorf("usage: %s", usage)
+
+ var room *string
+ if len(args) == 1 {
+ room = &args[0]
+ }
+
+ resp, err := c.client.ListBlacklistPolicies(ctx, &v1.ListBlacklistPoliciesRequest{
+ Room: room,
+ })
+ if err != nil {
+ return err
+ }
+
+ for _, policy := range resp.Policies {
+ name, has := matchModeToName[policy.Mode]
+ if !has {
+ name = "unknown"
+ }
+
+ println(name + " " + policy.Keyword)
}
+
+ return nil
+}
+
+func cmdUsageMsg(cmd Cmd) string {
+ if cmd.Usage == "" {
+ return "usage: " + cmd.Name
+ }
+
+ return "usage: " + cmd.Name + " " + cmd.Usage
+}
+
+func validateArgCount(args []string, min int, max int, cmd Cmd) error {
+ if len(args) < min || (max >= 0 && len(args) > max) {
+ return errors.New(cmdUsageMsg(cmd))
+ }
+
return nil
}
diff --git a/rpcclient/go.mod b/rpcclient/go.mod
index cc8db9da..aef23922 100644
--- a/rpcclient/go.mod
+++ b/rpcclient/go.mod
@@ -1,6 +1,6 @@
module friendnet.org/rpcclient
-go 1.26.2
+go 1.26.5
require (
connectrpc.com/connect v1.19.1
diff --git a/server-widget/pb/serverrpc/v1/rpc_pb.ts b/server-widget/pb/serverrpc/v1/rpc_pb.ts
index dadec738..b5c6e454 100644
--- a/server-widget/pb/serverrpc/v1/rpc_pb.ts
+++ b/server-widget/pb/serverrpc/v1/rpc_pb.ts
@@ -2,15 +2,15 @@
// @generated from file pb/serverrpc/v1/rpc.proto (package pb.serverrpc.v1, syntax proto3)
/* eslint-disable */
-import type { GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
-import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
+import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
+import { enumDesc, fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2";
import type { Message } from "@bufbuild/protobuf";
/**
* Describes the file pb/serverrpc/v1/rpc.proto.
*/
export const file_pb_serverrpc_v1_rpc: GenFile = /*@__PURE__*/
- fileDesc("ChlwYi9zZXJ2ZXJycGMvdjEvcnBjLnByb3RvEg9wYi5zZXJ2ZXJycGMudjEiMwoIUm9vbUluZm8SDAoEbmFtZRgBIAEoCRIZChFvbmxpbmVfdXNlcl9jb3VudBgCIAEoDSIiCg5PbmxpbmVVc2VySW5mbxIQCgh1c2VybmFtZRgBIAEoCSIfCgtBY2NvdW50SW5mbxIQCgh1c2VybmFtZRgBIAEoCSIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKgAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSNwoDcnBjGAIgASgLMioucGIuc2VydmVycnBjLnYxLkdldFNlcnZlckluZm9SZXNwb25zZS5ScGMaPQoDUnBjEhcKD2FsbG93ZWRfbWV0aG9kcxgBIAMoCRIdChVyZXF1aXJlc19iZWFyZXJfdG9rZW4YAiABKAgiEQoPR2V0Um9vbXNSZXF1ZXN0IjwKEEdldFJvb21zUmVzcG9uc2USKAoFcm9vbXMYASADKAsyGS5wYi5zZXJ2ZXJycGMudjEuUm9vbUluZm8iIgoSR2V0Um9vbUluZm9SZXF1ZXN0EgwKBG5hbWUYASABKAkiPgoTR2V0Um9vbUluZm9SZXNwb25zZRInCgRyb29tGAEgASgLMhkucGIuc2VydmVycnBjLnYxLlJvb21JbmZvIiUKFUdldE9ubGluZVVzZXJzUmVxdWVzdBIMCgRyb29tGAEgASgJIkgKFkdldE9ubGluZVVzZXJzUmVzcG9uc2USLgoFdXNlcnMYASADKAsyHy5wYi5zZXJ2ZXJycGMudjEuT25saW5lVXNlckluZm8iOgoYR2V0T25saW5lVXNlckluZm9SZXF1ZXN0EgwKBHJvb20YASABKAkSEAoIdXNlcm5hbWUYAiABKAkiSgoZR2V0T25saW5lVXNlckluZm9SZXNwb25zZRItCgR1c2VyGAEgASgLMh8ucGIuc2VydmVycnBjLnYxLk9ubGluZVVzZXJJbmZvIiIKEkdldEFjY291bnRzUmVxdWVzdBIMCgRyb29tGAEgASgJIkUKE0dldEFjY291bnRzUmVzcG9uc2USLgoIYWNjb3VudHMYASADKAsyHC5wYi5zZXJ2ZXJycGMudjEuQWNjb3VudEluZm8iIQoRQ3JlYXRlUm9vbVJlcXVlc3QSDAoEbmFtZRgBIAEoCSI9ChJDcmVhdGVSb29tUmVzcG9uc2USJwoEcm9vbRgBIAEoCzIZLnBiLnNlcnZlcnJwYy52MS5Sb29tSW5mbyIhChFEZWxldGVSb29tUmVxdWVzdBIMCgRuYW1lGAEgASgJIhQKEkRlbGV0ZVJvb21SZXNwb25zZSJIChRDcmVhdGVBY2NvdW50UmVxdWVzdBIMCgRyb29tGAEgASgJEhAKCHVzZXJuYW1lGAIgASgJEhAKCHBhc3N3b3JkGAMgASgJIn4KFUNyZWF0ZUFjY291bnRSZXNwb25zZRItCgdhY2NvdW50GAEgASgLMhwucGIuc2VydmVycnBjLnYxLkFjY291bnRJbmZvEh8KEmdlbmVyYXRlZF9wYXNzd29yZBgCIAEoCUgAiAEBQhUKE19nZW5lcmF0ZWRfcGFzc3dvcmQiNgoURGVsZXRlQWNjb3VudFJlcXVlc3QSDAoEcm9vbRgBIAEoCRIQCgh1c2VybmFtZRgCIAEoCSIXChVEZWxldGVBY2NvdW50UmVzcG9uc2UiUAocVXBkYXRlQWNjb3VudFBhc3N3b3JkUmVxdWVzdBIMCgRyb29tGAEgASgJEhAKCHVzZXJuYW1lGAIgASgJEhAKCHBhc3N3b3JkGAMgASgJIlcKHVVwZGF0ZUFjY291bnRQYXNzd29yZFJlc3BvbnNlEh8KEmdlbmVyYXRlZF9wYXNzd29yZBgBIAEoCUgAiAEBQhUKE19nZW5lcmF0ZWRfcGFzc3dvcmQyxAgKEFNlcnZlclJwY1NlcnZpY2USYAoNR2V0U2VydmVySW5mbxIlLnBiLnNlcnZlcnJwYy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBomLnBiLnNlcnZlcnJwYy52MS5HZXRTZXJ2ZXJJbmZvUmVzcG9uc2UiABJRCghHZXRSb29tcxIgLnBiLnNlcnZlcnJwYy52MS5HZXRSb29tc1JlcXVlc3QaIS5wYi5zZXJ2ZXJycGMudjEuR2V0Um9vbXNSZXNwb25zZSIAEloKC0dldFJvb21JbmZvEiMucGIuc2VydmVycnBjLnYxLkdldFJvb21JbmZvUmVxdWVzdBokLnBiLnNlcnZlcnJwYy52MS5HZXRSb29tSW5mb1Jlc3BvbnNlIgASZQoOR2V0T25saW5lVXNlcnMSJi5wYi5zZXJ2ZXJycGMudjEuR2V0T25saW5lVXNlcnNSZXF1ZXN0GicucGIuc2VydmVycnBjLnYxLkdldE9ubGluZVVzZXJzUmVzcG9uc2UiADABEmwKEUdldE9ubGluZVVzZXJJbmZvEikucGIuc2VydmVycnBjLnYxLkdldE9ubGluZVVzZXJJbmZvUmVxdWVzdBoqLnBiLnNlcnZlcnJwYy52MS5HZXRPbmxpbmVVc2VySW5mb1Jlc3BvbnNlIgASWgoLR2V0QWNjb3VudHMSIy5wYi5zZXJ2ZXJycGMudjEuR2V0QWNjb3VudHNSZXF1ZXN0GiQucGIuc2VydmVycnBjLnYxLkdldEFjY291bnRzUmVzcG9uc2UiABJXCgpDcmVhdGVSb29tEiIucGIuc2VydmVycnBjLnYxLkNyZWF0ZVJvb21SZXF1ZXN0GiMucGIuc2VydmVycnBjLnYxLkNyZWF0ZVJvb21SZXNwb25zZSIAElcKCkRlbGV0ZVJvb20SIi5wYi5zZXJ2ZXJycGMudjEuRGVsZXRlUm9vbVJlcXVlc3QaIy5wYi5zZXJ2ZXJycGMudjEuRGVsZXRlUm9vbVJlc3BvbnNlIgASYAoNQ3JlYXRlQWNjb3VudBIlLnBiLnNlcnZlcnJwYy52MS5DcmVhdGVBY2NvdW50UmVxdWVzdBomLnBiLnNlcnZlcnJwYy52MS5DcmVhdGVBY2NvdW50UmVzcG9uc2UiABJgCg1EZWxldGVBY2NvdW50EiUucGIuc2VydmVycnBjLnYxLkRlbGV0ZUFjY291bnRSZXF1ZXN0GiYucGIuc2VydmVycnBjLnYxLkRlbGV0ZUFjY291bnRSZXNwb25zZSIAEngKFVVwZGF0ZUFjY291bnRQYXNzd29yZBItLnBiLnNlcnZlcnJwYy52MS5VcGRhdGVBY2NvdW50UGFzc3dvcmRSZXF1ZXN0Gi4ucGIuc2VydmVycnBjLnYxLlVwZGF0ZUFjY291bnRQYXNzd29yZFJlc3BvbnNlIgBCIlogZnJpZW5kbmV0Lm9yZy9wcm90b2NvbC9zZXJ2ZXJycGNiBnByb3RvMw");
+ fileDesc("ChlwYi9zZXJ2ZXJycGMvdjEvcnBjLnByb3RvEg9wYi5zZXJ2ZXJycGMudjEiMwoIUm9vbUluZm8SDAoEbmFtZRgBIAEoCRIZChFvbmxpbmVfdXNlcl9jb3VudBgCIAEoDSIiCg5PbmxpbmVVc2VySW5mbxIQCgh1c2VybmFtZRgBIAEoCSIfCgtBY2NvdW50SW5mbxIQCgh1c2VybmFtZRgBIAEoCSIWChRHZXRTZXJ2ZXJJbmZvUmVxdWVzdCKgAQoVR2V0U2VydmVySW5mb1Jlc3BvbnNlEg8KB3ZlcnNpb24YASABKAkSNwoDcnBjGAIgASgLMioucGIuc2VydmVycnBjLnYxLkdldFNlcnZlckluZm9SZXNwb25zZS5ScGMaPQoDUnBjEhcKD2FsbG93ZWRfbWV0aG9kcxgBIAMoCRIdChVyZXF1aXJlc19iZWFyZXJfdG9rZW4YAiABKAgiEQoPR2V0Um9vbXNSZXF1ZXN0IjwKEEdldFJvb21zUmVzcG9uc2USKAoFcm9vbXMYASADKAsyGS5wYi5zZXJ2ZXJycGMudjEuUm9vbUluZm8iIgoSR2V0Um9vbUluZm9SZXF1ZXN0EgwKBG5hbWUYASABKAkiPgoTR2V0Um9vbUluZm9SZXNwb25zZRInCgRyb29tGAEgASgLMhkucGIuc2VydmVycnBjLnYxLlJvb21JbmZvIiUKFUdldE9ubGluZVVzZXJzUmVxdWVzdBIMCgRyb29tGAEgASgJIkgKFkdldE9ubGluZVVzZXJzUmVzcG9uc2USLgoFdXNlcnMYASADKAsyHy5wYi5zZXJ2ZXJycGMudjEuT25saW5lVXNlckluZm8iOgoYR2V0T25saW5lVXNlckluZm9SZXF1ZXN0EgwKBHJvb20YASABKAkSEAoIdXNlcm5hbWUYAiABKAkiSgoZR2V0T25saW5lVXNlckluZm9SZXNwb25zZRItCgR1c2VyGAEgASgLMh8ucGIuc2VydmVycnBjLnYxLk9ubGluZVVzZXJJbmZvIiIKEkdldEFjY291bnRzUmVxdWVzdBIMCgRyb29tGAEgASgJIkUKE0dldEFjY291bnRzUmVzcG9uc2USLgoIYWNjb3VudHMYASADKAsyHC5wYi5zZXJ2ZXJycGMudjEuQWNjb3VudEluZm8iIQoRQ3JlYXRlUm9vbVJlcXVlc3QSDAoEbmFtZRgBIAEoCSI9ChJDcmVhdGVSb29tUmVzcG9uc2USJwoEcm9vbRgBIAEoCzIZLnBiLnNlcnZlcnJwYy52MS5Sb29tSW5mbyIhChFEZWxldGVSb29tUmVxdWVzdBIMCgRuYW1lGAEgASgJIhQKEkRlbGV0ZVJvb21SZXNwb25zZSJIChRDcmVhdGVBY2NvdW50UmVxdWVzdBIMCgRyb29tGAEgASgJEhAKCHVzZXJuYW1lGAIgASgJEhAKCHBhc3N3b3JkGAMgASgJIn4KFUNyZWF0ZUFjY291bnRSZXNwb25zZRItCgdhY2NvdW50GAEgASgLMhwucGIuc2VydmVycnBjLnYxLkFjY291bnRJbmZvEh8KEmdlbmVyYXRlZF9wYXNzd29yZBgCIAEoCUgAiAEBQhUKE19nZW5lcmF0ZWRfcGFzc3dvcmQiNgoURGVsZXRlQWNjb3VudFJlcXVlc3QSDAoEcm9vbRgBIAEoCRIQCgh1c2VybmFtZRgCIAEoCSIXChVEZWxldGVBY2NvdW50UmVzcG9uc2UiUAocVXBkYXRlQWNjb3VudFBhc3N3b3JkUmVxdWVzdBIMCgRyb29tGAEgASgJEhAKCHVzZXJuYW1lGAIgASgJEhAKCHBhc3N3b3JkGAMgASgJIlcKHVVwZGF0ZUFjY291bnRQYXNzd29yZFJlc3BvbnNlEh8KEmdlbmVyYXRlZF9wYXNzd29yZBgBIAEoCUgAiAEBQhUKE19nZW5lcmF0ZWRfcGFzc3dvcmQiVQoPQmxhY2tsaXN0UG9saWN5Eg8KB2tleXdvcmQYASABKAkSMQoEbW9kZRgCIAEoDjIjLnBiLnNlcnZlcnJwYy52MS5CbGFja2xpc3RNYXRjaE1vZGUibQobQWRkQmxhY2tsaXN0UG9saWNpZXNSZXF1ZXN0EhEKBHJvb20YASABKAlIAIgBARIyCghwb2xpY2llcxgCIAMoCzIgLnBiLnNlcnZlcnJwYy52MS5CbGFja2xpc3RQb2xpY3lCBwoFX3Jvb20iHgocQWRkQmxhY2tsaXN0UG9saWNpZXNSZXNwb25zZSJOCh5SZW1vdmVCbGFja2xpc3RQb2xpY2llc1JlcXVlc3QSEQoEcm9vbRgBIAEoCUgAiAEBEhAKCHBvbGljaWVzGAIgAygJQgcKBV9yb29tIiEKH1JlbW92ZUJsYWNrbGlzdFBvbGljaWVzUmVzcG9uc2UiOgocTGlzdEJsYWNrbGlzdFBvbGljaWVzUmVxdWVzdBIRCgRyb29tGAEgASgJSACIAQFCBwoFX3Jvb20iUwodTGlzdEJsYWNrbGlzdFBvbGljaWVzUmVzcG9uc2USMgoIcG9saWNpZXMYASADKAsyIC5wYi5zZXJ2ZXJycGMudjEuQmxhY2tsaXN0UG9saWN5Kp4BChJCbGFja2xpc3RNYXRjaE1vZGUSJAogQkxBQ0tMSVNUX01BVENIX01PREVfVU5TUEVDSUZJRUQQABIiCh5CTEFDS0xJU1RfTUFUQ0hfTU9ERV9TVUJTVFJJTkcQARIeChpCTEFDS0xJU1RfTUFUQ0hfTU9ERV9XSE9MRRACEh4KGkJMQUNLTElTVF9NQVRDSF9NT0RFX1JFR0VYEAMytQsKEFNlcnZlclJwY1NlcnZpY2USYAoNR2V0U2VydmVySW5mbxIlLnBiLnNlcnZlcnJwYy52MS5HZXRTZXJ2ZXJJbmZvUmVxdWVzdBomLnBiLnNlcnZlcnJwYy52MS5HZXRTZXJ2ZXJJbmZvUmVzcG9uc2UiABJRCghHZXRSb29tcxIgLnBiLnNlcnZlcnJwYy52MS5HZXRSb29tc1JlcXVlc3QaIS5wYi5zZXJ2ZXJycGMudjEuR2V0Um9vbXNSZXNwb25zZSIAEloKC0dldFJvb21JbmZvEiMucGIuc2VydmVycnBjLnYxLkdldFJvb21JbmZvUmVxdWVzdBokLnBiLnNlcnZlcnJwYy52MS5HZXRSb29tSW5mb1Jlc3BvbnNlIgASZQoOR2V0T25saW5lVXNlcnMSJi5wYi5zZXJ2ZXJycGMudjEuR2V0T25saW5lVXNlcnNSZXF1ZXN0GicucGIuc2VydmVycnBjLnYxLkdldE9ubGluZVVzZXJzUmVzcG9uc2UiADABEmwKEUdldE9ubGluZVVzZXJJbmZvEikucGIuc2VydmVycnBjLnYxLkdldE9ubGluZVVzZXJJbmZvUmVxdWVzdBoqLnBiLnNlcnZlcnJwYy52MS5HZXRPbmxpbmVVc2VySW5mb1Jlc3BvbnNlIgASWgoLR2V0QWNjb3VudHMSIy5wYi5zZXJ2ZXJycGMudjEuR2V0QWNjb3VudHNSZXF1ZXN0GiQucGIuc2VydmVycnBjLnYxLkdldEFjY291bnRzUmVzcG9uc2UiABJXCgpDcmVhdGVSb29tEiIucGIuc2VydmVycnBjLnYxLkNyZWF0ZVJvb21SZXF1ZXN0GiMucGIuc2VydmVycnBjLnYxLkNyZWF0ZVJvb21SZXNwb25zZSIAElcKCkRlbGV0ZVJvb20SIi5wYi5zZXJ2ZXJycGMudjEuRGVsZXRlUm9vbVJlcXVlc3QaIy5wYi5zZXJ2ZXJycGMudjEuRGVsZXRlUm9vbVJlc3BvbnNlIgASYAoNQ3JlYXRlQWNjb3VudBIlLnBiLnNlcnZlcnJwYy52MS5DcmVhdGVBY2NvdW50UmVxdWVzdBomLnBiLnNlcnZlcnJwYy52MS5DcmVhdGVBY2NvdW50UmVzcG9uc2UiABJgCg1EZWxldGVBY2NvdW50EiUucGIuc2VydmVycnBjLnYxLkRlbGV0ZUFjY291bnRSZXF1ZXN0GiYucGIuc2VydmVycnBjLnYxLkRlbGV0ZUFjY291bnRSZXNwb25zZSIAEngKFVVwZGF0ZUFjY291bnRQYXNzd29yZBItLnBiLnNlcnZlcnJwYy52MS5VcGRhdGVBY2NvdW50UGFzc3dvcmRSZXF1ZXN0Gi4ucGIuc2VydmVycnBjLnYxLlVwZGF0ZUFjY291bnRQYXNzd29yZFJlc3BvbnNlIgASdQoUQWRkQmxhY2tsaXN0UG9saWNpZXMSLC5wYi5zZXJ2ZXJycGMudjEuQWRkQmxhY2tsaXN0UG9saWNpZXNSZXF1ZXN0Gi0ucGIuc2VydmVycnBjLnYxLkFkZEJsYWNrbGlzdFBvbGljaWVzUmVzcG9uc2UiABJ+ChdSZW1vdmVCbGFja2xpc3RQb2xpY2llcxIvLnBiLnNlcnZlcnJwYy52MS5SZW1vdmVCbGFja2xpc3RQb2xpY2llc1JlcXVlc3QaMC5wYi5zZXJ2ZXJycGMudjEuUmVtb3ZlQmxhY2tsaXN0UG9saWNpZXNSZXNwb25zZSIAEngKFUxpc3RCbGFja2xpc3RQb2xpY2llcxItLnBiLnNlcnZlcnJwYy52MS5MaXN0QmxhY2tsaXN0UG9saWNpZXNSZXF1ZXN0Gi4ucGIuc2VydmVycnBjLnYxLkxpc3RCbGFja2xpc3RQb2xpY2llc1Jlc3BvbnNlIgBCIlogZnJpZW5kbmV0Lm9yZy9wcm90b2NvbC9zZXJ2ZXJycGNiBnByb3RvMw");
/**
* RoomInfo is information about a room.
@@ -557,6 +557,189 @@ export type UpdateAccountPasswordResponse = Message<"pb.serverrpc.v1.UpdateAccou
export const UpdateAccountPasswordResponseSchema: GenMessage = /*@__PURE__*/
messageDesc(file_pb_serverrpc_v1_rpc, 24);
+/**
+ * BlacklistPolicy is a word and the policy for how it will be enforced in a blacklist.
+ *
+ * @generated from message pb.serverrpc.v1.BlacklistPolicy
+ */
+export type BlacklistPolicy = Message<"pb.serverrpc.v1.BlacklistPolicy"> & {
+ /**
+ * The keyword.
+ *
+ * @generated from field: string keyword = 1;
+ */
+ keyword: string;
+
+ /**
+ * How the keyword will be matched.
+ *
+ * @generated from field: pb.serverrpc.v1.BlacklistMatchMode mode = 2;
+ */
+ mode: BlacklistMatchMode;
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.BlacklistPolicy.
+ * Use `create(BlacklistPolicySchema)` to create a new message.
+ */
+export const BlacklistPolicySchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 25);
+
+/**
+ * @generated from message pb.serverrpc.v1.AddBlacklistPoliciesRequest
+ */
+export type AddBlacklistPoliciesRequest = Message<"pb.serverrpc.v1.AddBlacklistPoliciesRequest"> & {
+ /**
+ * Room to enforce this policy. If null, then it is enforced serverwide.
+ *
+ * @generated from field: optional string room = 1;
+ */
+ room?: string;
+
+ /**
+ * The policies to add.
+ *
+ * @generated from field: repeated pb.serverrpc.v1.BlacklistPolicy policies = 2;
+ */
+ policies: BlacklistPolicy[];
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.AddBlacklistPoliciesRequest.
+ * Use `create(AddBlacklistPoliciesRequestSchema)` to create a new message.
+ */
+export const AddBlacklistPoliciesRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 26);
+
+/**
+ * @generated from message pb.serverrpc.v1.AddBlacklistPoliciesResponse
+ */
+export type AddBlacklistPoliciesResponse = Message<"pb.serverrpc.v1.AddBlacklistPoliciesResponse"> & {
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.AddBlacklistPoliciesResponse.
+ * Use `create(AddBlacklistPoliciesResponseSchema)` to create a new message.
+ */
+export const AddBlacklistPoliciesResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 27);
+
+/**
+ * @generated from message pb.serverrpc.v1.RemoveBlacklistPoliciesRequest
+ */
+export type RemoveBlacklistPoliciesRequest = Message<"pb.serverrpc.v1.RemoveBlacklistPoliciesRequest"> & {
+ /**
+ * Room in which this policy is enforced. If null, it is assumed to be serverwide.
+ *
+ * @generated from field: optional string room = 1;
+ */
+ room?: string;
+
+ /**
+ * The policies to remove (identified by their keywords).
+ *
+ * @generated from field: repeated string policies = 2;
+ */
+ policies: string[];
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.RemoveBlacklistPoliciesRequest.
+ * Use `create(RemoveBlacklistPoliciesRequestSchema)` to create a new message.
+ */
+export const RemoveBlacklistPoliciesRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 28);
+
+/**
+ * @generated from message pb.serverrpc.v1.RemoveBlacklistPoliciesResponse
+ */
+export type RemoveBlacklistPoliciesResponse = Message<"pb.serverrpc.v1.RemoveBlacklistPoliciesResponse"> & {
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.RemoveBlacklistPoliciesResponse.
+ * Use `create(RemoveBlacklistPoliciesResponseSchema)` to create a new message.
+ */
+export const RemoveBlacklistPoliciesResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 29);
+
+/**
+ * @generated from message pb.serverrpc.v1.ListBlacklistPoliciesRequest
+ */
+export type ListBlacklistPoliciesRequest = Message<"pb.serverrpc.v1.ListBlacklistPoliciesRequest"> & {
+ /**
+ * If null, it will return only global policies.
+ *
+ * @generated from field: optional string room = 1;
+ */
+ room?: string;
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.ListBlacklistPoliciesRequest.
+ * Use `create(ListBlacklistPoliciesRequestSchema)` to create a new message.
+ */
+export const ListBlacklistPoliciesRequestSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 30);
+
+/**
+ * @generated from message pb.serverrpc.v1.ListBlacklistPoliciesResponse
+ */
+export type ListBlacklistPoliciesResponse = Message<"pb.serverrpc.v1.ListBlacklistPoliciesResponse"> & {
+ /**
+ * The policies.
+ *
+ * @generated from field: repeated pb.serverrpc.v1.BlacklistPolicy policies = 1;
+ */
+ policies: BlacklistPolicy[];
+};
+
+/**
+ * Describes the message pb.serverrpc.v1.ListBlacklistPoliciesResponse.
+ * Use `create(ListBlacklistPoliciesResponseSchema)` to create a new message.
+ */
+export const ListBlacklistPoliciesResponseSchema: GenMessage = /*@__PURE__*/
+ messageDesc(file_pb_serverrpc_v1_rpc, 31);
+
+/**
+ * BlacklistMatchMode are the possible modes for matching a word in a blacklist.
+ *
+ * @generated from enum pb.serverrpc.v1.BlacklistMatchMode
+ */
+export enum BlacklistMatchMode {
+ /**
+ * @generated from enum value: BLACKLIST_MATCH_MODE_UNSPECIFIED = 0;
+ */
+ UNSPECIFIED = 0,
+
+ /**
+ * Matches any substring of the word.
+ *
+ * @generated from enum value: BLACKLIST_MATCH_MODE_SUBSTRING = 1;
+ */
+ SUBSTRING = 1,
+
+ /**
+ * Matches only whole words that match.
+ *
+ * @generated from enum value: BLACKLIST_MATCH_MODE_WHOLE = 2;
+ */
+ WHOLE = 2,
+
+ /**
+ * Matches terms with regex.
+ *
+ * @generated from enum value: BLACKLIST_MATCH_MODE_REGEX = 3;
+ */
+ REGEX = 3,
+}
+
+/**
+ * Describes the enum pb.serverrpc.v1.BlacklistMatchMode.
+ */
+export const BlacklistMatchModeSchema: GenEnum = /*@__PURE__*/
+ enumDesc(file_pb_serverrpc_v1_rpc, 0);
+
/**
* ServerRpcService provides an RPC interface to a running FriendNet server.
* It can query state and perform administrative tasks.
@@ -694,6 +877,41 @@ export const ServerRpcService: GenService<{
input: typeof UpdateAccountPasswordRequestSchema;
output: typeof UpdateAccountPasswordResponseSchema;
},
+ /**
+ * AddBlacklistPolicies adds one or more keywords that will be blacklisted from search queries and filenames.
+ * If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ * Returns status code INVALID_ARGUMENT if the keyword is empty or invalid for the specified match mode.
+ *
+ * @generated from rpc pb.serverrpc.v1.ServerRpcService.AddBlacklistPolicies
+ */
+ addBlacklistPolicies: {
+ methodKind: "unary";
+ input: typeof AddBlacklistPoliciesRequestSchema;
+ output: typeof AddBlacklistPoliciesResponseSchema;
+ },
+ /**
+ * RemoveBlacklistPolicies removes one or more keyword policies from the blacklists.
+ * If a room to enforce this policy is not specified then they are assumed to be serverwide.
+ *
+ * @generated from rpc pb.serverrpc.v1.ServerRpcService.RemoveBlacklistPolicies
+ */
+ removeBlacklistPolicies: {
+ methodKind: "unary";
+ input: typeof RemoveBlacklistPoliciesRequestSchema;
+ output: typeof RemoveBlacklistPoliciesResponseSchema;
+ },
+ /**
+ * ListBlacklistPolicies returns a list of currently blacklisted keywords for a room.
+ * If the room is not set, the list returned will contain only serverwide policies.
+ * Returns status code NOT_FOUND if the room does not exist.
+ *
+ * @generated from rpc pb.serverrpc.v1.ServerRpcService.ListBlacklistPolicies
+ */
+ listBlacklistPolicies: {
+ methodKind: "unary";
+ input: typeof ListBlacklistPoliciesRequestSchema;
+ output: typeof ListBlacklistPoliciesResponseSchema;
+ },
}> = /*@__PURE__*/
serviceDesc(file_pb_serverrpc_v1_rpc, 0);
diff --git a/server/blacklist/blacklist.go b/server/blacklist/blacklist.go
new file mode 100644
index 00000000..96cde46d
--- /dev/null
+++ b/server/blacklist/blacklist.go
@@ -0,0 +1,226 @@
+package blacklist
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "regexp"
+ "sync"
+
+ anyascii "github.com/anyascii/go"
+
+ "friendnet.org/ahocorasick"
+ "friendnet.org/common"
+ pb "friendnet.org/protocol/pb/serverrpc/v1"
+)
+
+// Blacklist stores blocked keywords.
+// This object maintains a string matching engine through a persistent table in storage.
+// On server start, it will fetch and build the engine from said storage.
+// This object can also be dynamically updated to add/remove words to/from the engine.
+type Blacklist struct {
+ mu sync.RWMutex
+
+ ctx context.Context
+ storage PolicyStorage
+ machine *ahocorasick.Machine
+
+ wholeWords map[string]struct{}
+ hasAnyKeywords bool
+ regexes []*regexp.Regexp
+}
+
+// New creates a new blacklist.
+func New(ctx context.Context, storage PolicyStorage) (*Blacklist, error) {
+ if storage == nil {
+ return nil, fmt.Errorf("storage is nil")
+ }
+
+ machine := new(ahocorasick.Machine)
+
+ blacklist := &Blacklist{
+ ctx: ctx,
+ storage: storage,
+ machine: machine,
+
+ wholeWords: make(map[string]struct{}),
+ hasAnyKeywords: false,
+ }
+
+ err := blacklist.UpdateFromDb()
+ if err != nil {
+ return nil, fmt.Errorf("UpdateFromDb failed: %w", err)
+ }
+
+ return blacklist, nil
+}
+
+// UpdateFromDb will update the string matching engine with all matching policies.
+func (b *Blacklist) UpdateFromDb() error {
+ policies, err := b.storage.GetPolicies(b.ctx)
+ if err != nil {
+ return fmt.Errorf("could not get blacklisted policies: %w", err)
+ }
+
+ b.mu.Lock()
+ defer b.mu.Unlock()
+
+ // Set it to false until we know the build succeeded.
+ b.hasAnyKeywords = false
+
+ if len(policies) == 0 {
+ return nil
+ }
+
+ trieKeywords := make([][]rune, 0, len(policies))
+ clear(b.wholeWords)
+ clear(b.regexes)
+
+ for _, policy := range policies {
+ switch policy.Mode {
+ case pb.BlacklistMatchMode_BLACKLIST_MATCH_MODE_WHOLE:
+ kw := common.ToLowerUnicode(policy.Keyword)
+ trieKeywords = append(trieKeywords, []rune(kw))
+ b.wholeWords[kw] = struct{}{}
+ case pb.BlacklistMatchMode_BLACKLIST_MATCH_MODE_SUBSTRING:
+ trieKeywords = append(trieKeywords, []rune(common.ToLowerUnicode(policy.Keyword)))
+ case pb.BlacklistMatchMode_BLACKLIST_MATCH_MODE_REGEX:
+ regex, err := regexp.Compile(policy.Keyword)
+ if err != nil {
+ return fmt.Errorf(`encountered invalid regex %q when loading blacklist policies: %w`,
+ policy.Keyword,
+ err,
+ )
+ }
+
+ b.regexes = append(b.regexes, regex)
+ default:
+ return fmt.Errorf(`encountered unknown match mode %d when loading blacklist policies`, policy.Mode)
+ }
+ }
+
+ err = b.machine.Build(trieKeywords)
+ if err != nil {
+ return err
+ }
+
+ b.hasAnyKeywords = true
+
+ return nil
+}
+
+// ErrEmptyKeyword is returned when trying to create a policy with an empty keyword.
+var ErrEmptyKeyword = errors.New("tried to add blacklist policy with empty keyword")
+
+// AddPolicies will add blacklist policies to the database and then update the string matching engine.
+// It does not validate policies other than making sure that their keywords aren't empty.
+func (b *Blacklist) AddPolicies(policies []*pb.BlacklistPolicy) error {
+ for _, policy := range policies {
+ if policy.Keyword == "" {
+ return ErrEmptyKeyword
+ }
+ }
+
+ err := b.storage.AddPolicies(b.ctx, policies)
+ if err != nil {
+ return err
+ }
+
+ return b.UpdateFromDb()
+}
+
+// Remove will remove keywords from the database and then update the string matching engine.
+func (b *Blacklist) Remove(keywords []string) error {
+ err := b.storage.RemovePolicies(b.ctx, keywords)
+ if err != nil {
+ return err
+ }
+
+ return b.UpdateFromDb()
+}
+
+func isCharAsciiWord[R rune | byte](r R) bool {
+ return (r >= 'A' && r <= 'Z') ||
+ (r >= 'a' && r <= 'z') ||
+ (r >= '0' && r <= '9')
+}
+
+// Match runs a multi-pattern search on a given string and returns true if there is a match of any kind.
+// Haystack (both the []rune version and the string version) should be in lowercase.
+func (b *Blacklist) Match(haystackRunes []rune, haystackStr string) bool {
+ b.mu.RLock()
+ defer b.mu.RUnlock()
+
+ if !b.hasAnyKeywords {
+ return false
+ }
+
+ // Try words first.
+ results := b.machine.MultiPatternSearch(haystackRunes, true)
+
+ for _, result := range results {
+ wordStr := string(result.Word)
+
+ // Got a match.
+ // If this keyword should only match whole words, use that logic instead.
+ if _, has := b.wholeWords[wordStr]; has {
+ wordStart := result.Pos
+ wordEnd := wordStart + len(result.Word) - 1
+
+ // termer 2026/07/27: It's not exactly foolproof, but the idea is this:
+ // If a char is ASCII and the next char is an ASCII word char, then it's a substring match.
+ // If a char is unicode and the next char is unicode, then it's a substring match.
+ // That would allow a string like "blue天" to match full word "blue".
+ // It would also allow 藍天 to NOT match full word "天".
+ // We also try to detect and convert unicode punctuation to their ASCII counterparts.
+ // Things still don't work great for languages without spaces like CJK; substring should be used for those.
+
+ if wordStart > 0 {
+ startChar := haystackRunes[wordStart]
+ prevChar := haystackRunes[wordStart-1]
+
+ if startChar > 255 && prevChar > 255 {
+ // The previous unicode char could be punctuation; test if it is.
+ prevAscii := anyascii.TransliterateRune(prevChar)
+ if isCharAsciiWord(prevAscii[0]) {
+ // Matched a substring
+ continue
+ }
+ } else if startChar <= 255 && isCharAsciiWord(prevChar) {
+ // Matched a substring.
+ continue
+ }
+ }
+
+ if wordEnd < len(haystackRunes)-1 {
+ endChar := haystackRunes[wordEnd]
+ nextChar := haystackRunes[wordEnd+1]
+
+ if endChar > 255 && nextChar > 255 {
+ // The next unicode char could be punctuation; test if it is.
+ nextAscii := anyascii.TransliterateRune(nextChar)
+ if isCharAsciiWord(nextAscii[0]) {
+ // Matched a substring
+ continue
+ }
+ } else if endChar <= 255 && isCharAsciiWord(nextChar) {
+ // Matched a substring.
+ continue
+ }
+ }
+
+ return true
+ }
+
+ return true
+ }
+
+ // No words matched, try regex.
+ for _, regex := range b.regexes {
+ if regex.MatchString(haystackStr) {
+ return true
+ }
+ }
+
+ return false
+}
diff --git a/server/blacklist/blacklist_test.go b/server/blacklist/blacklist_test.go
new file mode 100644
index 00000000..bd355eb3
--- /dev/null
+++ b/server/blacklist/blacklist_test.go
@@ -0,0 +1,111 @@
+package blacklist
+
+import (
+ "context"
+ "testing"
+
+ pb "friendnet.org/protocol/pb/serverrpc/v1"
+)
+
+func mkBl() *Blacklist {
+ bl, err := New(context.Background(), NewMemoryStorage())
+ if err != nil {
+ panic(err)
+ }
+ return bl
+}
+
+func TestSubstring(t *testing.T) {
+ bl := mkBl()
+ err := bl.AddPolicies([]*pb.BlacklistPolicy{
+ {
+ Keyword: "blue",
+ Mode: pb.BlacklistMatchMode_BLACKLIST_MATCH_MODE_SUBSTRING,
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ if !bl.Match([]rune("thebluelight"), "thebluelight") {
+ t.Fatal("didn't match substring")
+ }
+}
+
+func TestWholeAscii(t *testing.T) {
+ bl := mkBl()
+ err := bl.AddPolicies([]*pb.BlacklistPolicy{
+ {
+ Keyword: "blue",
+ Mode: pb.BlacklistMatchMode_BLACKLIST_MATCH_MODE_WHOLE,
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ strs := []string{
+ "out of the blue",
+ "(blue)",
+ "blue",
+ }
+
+ for _, str := range strs {
+ if !bl.Match([]rune(str), str) {
+ t.Fatal("didn't match whole word in " + str)
+ }
+ }
+}
+
+func TestWholeAsciiWithUnicode(t *testing.T) {
+ bl := mkBl()
+ err := bl.AddPolicies([]*pb.BlacklistPolicy{
+ {
+ Keyword: "blue",
+ Mode: pb.BlacklistMatchMode_BLACKLIST_MATCH_MODE_WHOLE,
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ strs := []string{
+ "我的blue天",
+ "(blue)",
+ }
+
+ for _, str := range strs {
+ if !bl.Match([]rune(str), str) {
+ t.Fatal("didn't match whole word in " + str)
+ }
+ }
+}
+
+func TestWholeUnicode(t *testing.T) {
+ bl := mkBl()
+ err := bl.AddPolicies([]*pb.BlacklistPolicy{
+ {
+ Keyword: "кот",
+ Mode: pb.BlacklistMatchMode_BLACKLIST_MATCH_MODE_WHOLE,
+ },
+ })
+ if err != nil {
+ panic(err)
+ }
+
+ strs := []string{
+ "(кот)",
+ "кот",
+ "(кот)",
+ }
+
+ for _, str := range strs {
+ if !bl.Match([]rune(str), str) {
+ t.Fatal("didn't match whole word in " + str)
+ }
+ }
+
+ if bl.Match([]rune("котёл"), "котёл") {
+ t.Fatal("кот should not have matched substring of котёл")
+ }
+}
diff --git a/server/blacklist/storage.go b/server/blacklist/storage.go
new file mode 100644
index 00000000..b1525fda
--- /dev/null
+++ b/server/blacklist/storage.go
@@ -0,0 +1,138 @@
+package blacklist
+
+import (
+ "context"
+ "fmt"
+ "friendnet.org/common"
+ pb "friendnet.org/protocol/pb/serverrpc/v1"
+ "friendnet.org/server/storage"
+)
+
+// PolicyStorage is an interface that defines methods for storing and retrieving blacklist policies.
+type PolicyStorage interface {
+ // GetPolicies returns all blacklist policies.
+ GetPolicies(ctx context.Context) ([]*pb.BlacklistPolicy, error)
+
+ // AddPolicies adds one or more policies.
+ AddPolicies(ctx context.Context, policies []*pb.BlacklistPolicy) error
+
+ // RemovePolicies removes one or more policies by their keywords.
+ RemovePolicies(ctx context.Context, keywords []string) error
+}
+
+// GlobalStorage can store and retrieve global blacklist policies.
+type GlobalStorage struct {
+ storage *storage.Storage
+}
+
+var _ PolicyStorage = GlobalStorage{}
+
+// NewGlobalStorage creates a new GlobalStorage.
+func NewGlobalStorage(storage *storage.Storage) GlobalStorage {
+ return GlobalStorage{
+ storage: storage,
+ }
+}
+
+func (s GlobalStorage) GetPolicies(ctx context.Context) ([]*pb.BlacklistPolicy, error) {
+ policies, err := s.storage.GetBlacklistPoliciesForRoom(ctx, common.ZeroNormalizedRoomName)
+ if err != nil {
+ return nil, fmt.Errorf("could not get global blacklist policies: %w", err)
+ }
+
+ return policies, nil
+}
+
+func (s GlobalStorage) AddPolicies(ctx context.Context, policies []*pb.BlacklistPolicy) error {
+ err := s.storage.AddPoliciesToBlacklist(ctx, common.ZeroNormalizedRoomName, policies)
+ if err != nil {
+ return fmt.Errorf(`failed to add global blacklist policies: %w`, err)
+ }
+ return nil
+}
+
+func (s GlobalStorage) RemovePolicies(ctx context.Context, keywords []string) error {
+ err := s.storage.RemovePoliciesFromBlacklist(ctx, common.ZeroNormalizedRoomName, keywords)
+ if err != nil {
+ return fmt.Errorf(`failed to remove policies from global blacklist: %w`, err)
+ }
+ return nil
+}
+
+// RoomStorage can store and retrieve room-specific blacklist policies.
+type RoomStorage struct {
+ storage *storage.Storage
+ room common.NormalizedRoomName
+}
+
+var _ PolicyStorage = (*RoomStorage)(nil)
+
+// NewRoomStorage creates a new RoomStorage.
+func NewRoomStorage(storage *storage.Storage, room common.NormalizedRoomName) *RoomStorage {
+ return &RoomStorage{
+ storage: storage,
+ room: room,
+ }
+}
+
+func (s RoomStorage) GetPolicies(ctx context.Context) ([]*pb.BlacklistPolicy, error) {
+ policies, err := s.storage.GetBlacklistPoliciesForRoom(ctx, s.room)
+ if err != nil {
+ return nil, fmt.Errorf("could not get room %q blacklist policies: %w", s.room.String(), err)
+ }
+
+ return policies, nil
+}
+
+func (s RoomStorage) AddPolicies(ctx context.Context, policies []*pb.BlacklistPolicy) error {
+ err := s.storage.AddPoliciesToBlacklist(ctx, s.room, policies)
+ if err != nil {
+ return fmt.Errorf(`failed to add room %q blacklist policies: %w`, s.room.String(), err)
+ }
+ return nil
+}
+
+func (s RoomStorage) RemovePolicies(ctx context.Context, keywords []string) error {
+ err := s.storage.RemovePoliciesFromBlacklist(ctx, s.room, keywords)
+ if err != nil {
+ return fmt.Errorf(`failed to remove policies from room %q blacklist: %w`, s.room.String(), err)
+ }
+ return nil
+}
+
+// MemoryStorage can store and retrieve blacklist policies in memory.
+// Meant for testing, not threadsafe.
+type MemoryStorage struct {
+ policies map[string]*pb.BlacklistPolicy
+}
+
+var _ PolicyStorage = MemoryStorage{}
+
+// NewMemoryStorage creates a new MemoryStorage.
+func NewMemoryStorage() MemoryStorage {
+ return MemoryStorage{
+ policies: make(map[string]*pb.BlacklistPolicy),
+ }
+}
+
+func (s MemoryStorage) GetPolicies(_ context.Context) ([]*pb.BlacklistPolicy, error) {
+ res := make([]*pb.BlacklistPolicy, 0, len(s.policies))
+ for _, policy := range s.policies {
+ res = append(res, policy)
+ }
+ return res, nil
+}
+
+func (s MemoryStorage) AddPolicies(_ context.Context, policies []*pb.BlacklistPolicy) error {
+ for _, policy := range policies {
+ s.policies[policy.Keyword] = policy
+ }
+ return nil
+}
+
+func (s MemoryStorage) RemovePolicies(_ context.Context, keywords []string) error {
+ for _, key := range keywords {
+ s.policies[key] = nil
+ }
+ return nil
+}
diff --git a/server/go.mod b/server/go.mod
index 51546ac8..a8bf3831 100644
--- a/server/go.mod
+++ b/server/go.mod
@@ -5,11 +5,13 @@ go 1.26.5
require (
connectrpc.com/connect v1.19.1
friendnet.org/adminui v0.0.0
+ friendnet.org/ahocorasick v0.0.0-00010101000000-000000000000
friendnet.org/common v0.0.0
friendnet.org/protocol v0.0.0
friendnet.org/rpcclient v0.0.0
friendnet.org/stun v0.0.0
friendnet.org/updater v0.0.0
+ github.com/anyascii/go v0.3.3
github.com/quic-go/quic-go v0.61.0
github.com/termermc/go-mcf-password v1.0.0
golang.org/x/term v0.45.0
@@ -36,6 +38,7 @@ require (
replace (
friendnet.org/adminui => ../adminui
+ friendnet.org/ahocorasick => ../ahocorasick
friendnet.org/common => ../common
friendnet.org/protocol => ../protocol
friendnet.org/rpcclient => ../rpcclient
diff --git a/server/go.sum b/server/go.sum
index b50af6f8..4fbff0da 100644
--- a/server/go.sum
+++ b/server/go.sum
@@ -1,5 +1,7 @@
connectrpc.com/connect v1.19.1 h1:R5M57z05+90EfEvCY1b7hBxDVOUl45PrtXtAV2fOC14=
connectrpc.com/connect v1.19.1/go.mod h1:tN20fjdGlewnSFeZxLKb0xwIZ6ozc3OQs2hTXy4du9w=
+github.com/anyascii/go v0.3.3 h1:A3BhW92hXPYPb8Y1+zLOip8xjSxNcdl1FaxzTmFxrhs=
+github.com/anyascii/go v0.3.3/go.mod h1:HDvbMmSpqJyIe+xtSkHmAYTjc8PzvO3l1Jmgx/IFUPs=
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
diff --git a/server/room/logic.go b/server/room/logic.go
index d30555ae..8b140a2c 100644
--- a/server/room/logic.go
+++ b/server/room/logic.go
@@ -467,6 +467,14 @@ func (l LogicImpl) OnSearch(ctx context.Context, client *Client, bidi protocol.P
return
}
+ filterStr := next.DirectoryPath + "/" + next.File.Name
+ matches := c.Room.MatchToBlacklists(filterStr)
+
+ // This path+file matched the blacklist
+ if matches {
+ continue
+ }
+
select {
case <-timeoutCtx.Done():
return
diff --git a/server/room/manager.go b/server/room/manager.go
index 6f4dfd56..66448746 100644
--- a/server/room/manager.go
+++ b/server/room/manager.go
@@ -3,6 +3,7 @@ package room
import (
"context"
"fmt"
+ "friendnet.org/server/blacklist"
"log/slog"
"sync"
@@ -23,6 +24,7 @@ type Manager struct {
mu sync.RWMutex
isClosed bool
+ GlobalBlacklist *blacklist.Blacklist
storage *storage.Storage
connMethodSupport machine.ConnMethodSupport
passReqs password.Requirements
@@ -43,9 +45,15 @@ func NewManager(
passReqs password.Requirements,
logic Logic,
) (*Manager, error) {
+ bl, err := blacklist.New(ctx, blacklist.NewGlobalStorage(storage))
+ if err != nil {
+ return nil, fmt.Errorf("failed to init global keyword blacklist while creating room manager: %w", err)
+ }
+
m := &Manager{
logger: logger,
+ GlobalBlacklist: bl,
storage: storage,
connMethodSupport: connMethodSupport,
passReqs: passReqs,
@@ -61,14 +69,20 @@ func NewManager(
return nil, fmt.Errorf(`failed to get all rooms while creating new room manager: %w`, err)
}
for _, room := range rooms {
- m.rooms[room.Name.String()] = NewRoom(
+ newRoom, err := NewRoom(
logger,
storage,
connMethodSupport,
passReqs,
room.Name,
logic,
+ m.GlobalBlacklist,
)
+ if err != nil {
+ return nil, err
+ }
+
+ m.rooms[room.Name.String()] = newRoom
}
return m, nil
@@ -147,14 +161,18 @@ func (m *Manager) CreateRoom(ctx context.Context, name common.NormalizedRoomName
}
// Create room instance and add it to manager.
- room := NewRoom(
+ room, err := NewRoom(
m.logger,
m.storage,
m.connMethodSupport,
m.passReqs,
name,
m.logic,
+ m.GlobalBlacklist,
)
+ if err != nil {
+ return nil, err
+ }
m.mu.Lock()
m.rooms[name.String()] = room
diff --git a/server/room/proxy.go b/server/room/proxy.go
index b483d2fe..c1026a05 100644
--- a/server/room/proxy.go
+++ b/server/room/proxy.go
@@ -31,8 +31,6 @@ type ClientProxy struct {
targetBidi protocol.ProtoBidi
}
-const proxyBufSize = 1024
-
// NewClientProxy creates a new ClientProxy from an existing origin bidi.
// It assumes that the origin bidi has already had the open request message read from it, meaning the
// only data that will be sent on it will be proxied data.
diff --git a/server/room/room.go b/server/room/room.go
index 462b8817..7fda9250 100644
--- a/server/room/room.go
+++ b/server/room/room.go
@@ -14,6 +14,7 @@ import (
pass "friendnet.org/common/password"
"friendnet.org/protocol"
pb "friendnet.org/protocol/pb/v1"
+ "friendnet.org/server/blacklist"
"friendnet.org/server/storage"
"github.com/quic-go/quic-go"
mcfpassword "github.com/termermc/go-mcf-password"
@@ -42,6 +43,10 @@ type Room struct {
// The room's token manager.
TokenManager *TokenManager
+ // Keyword blacklists
+ GlobalBlacklist *blacklist.Blacklist
+ Blacklist *blacklist.Blacklist
+
// The room's context.
// Canceled when it is closed.
Context context.Context
@@ -62,9 +67,16 @@ func NewRoom(
passReqs pass.Requirements,
name common.NormalizedRoomName,
logic Logic,
-) *Room {
+ globalBlacklist *blacklist.Blacklist,
+) (*Room, error) {
ctx, ctxCancel := context.WithCancel(context.Background())
+ bl, err := blacklist.New(ctx, blacklist.NewRoomStorage(storage, name))
+ if err != nil {
+ ctxCancel()
+ return nil, err
+ }
+
return &Room{
logger: logger,
@@ -76,13 +88,16 @@ func NewRoom(
TokenManager: NewTokenManager(ctx, DefaultTokenValidDuration, DefaultTokenExpiredGcInterval),
+ Blacklist: bl,
+ GlobalBlacklist: globalBlacklist,
+
Context: ctx,
ctxCancel: ctxCancel,
logic: logic,
clients: make(map[string]*Client),
- }
+ }, nil
}
func (r *Room) snapshotClientsNoLock() []*Client {
@@ -166,6 +181,15 @@ func (r *Room) GetAllClients() []*Client {
return r.snapshotClientsNoLock()
}
+// MatchToBlacklists will match a string against room-local and global keyword blacklists.
+// It processes the string in multiple ways to improve matching.
+func (r *Room) MatchToBlacklists(haystack string) bool {
+ lower := common.ToLowerUnicode(haystack)
+ runes := []rune(lower)
+
+ return r.GlobalBlacklist.Match(runes, lower) || r.Blacklist.Match(runes, lower)
+}
+
// Broadcast broadcasts a message to all clients in the room.
// It is fire-and-forget and returns quickly, not waiting for the message to be sent.
// No-op if the room is closed.
diff --git a/server/rpc.go b/server/rpc.go
index 0caddcf6..90ee2f5a 100644
--- a/server/rpc.go
+++ b/server/rpc.go
@@ -11,6 +11,7 @@ import (
"friendnet.org/common/password"
v1 "friendnet.org/protocol/pb/serverrpc/v1"
"friendnet.org/protocol/pb/serverrpc/v1/serverrpcv1connect"
+ "friendnet.org/server/blacklist"
"friendnet.org/server/room"
"friendnet.org/server/storage"
"friendnet.org/updater"
@@ -23,6 +24,7 @@ var errRoomExists = connect.NewError(connect.CodeAlreadyExists, errors.New("room
var errAccountExists = connect.NewError(connect.CodeAlreadyExists, errors.New("account already exists"))
var errInvalidRoomName = connect.NewError(connect.CodeInvalidArgument, errors.New("invalid room name"))
var errInvalidUsername = connect.NewError(connect.CodeInvalidArgument, errors.New("invalid username"))
+var errEmptyPolicyKeyword = connect.NewError(connect.CodeInvalidArgument, blacklist.ErrEmptyKeyword)
type RpcServer struct {
s *Server
@@ -88,6 +90,7 @@ func (s *RpcServer) getClient(r *room.Room, username string) (*room.Client, erro
return client, nil
}
+
func (s *RpcServer) getOrGenPass(pass string) (string, bool) {
if pass == "" {
var buf [12]byte
@@ -320,3 +323,78 @@ func (s *RpcServer) GetServerInfo(_ context.Context, _ *v1.GetServerInfoRequest)
},
}, nil
}
+
+func (s *RpcServer) AddBlacklistPolicies(_ context.Context, req *v1.AddBlacklistPoliciesRequest) (*v1.AddBlacklistPoliciesResponse, error) {
+ // Room scoped policy
+ if req.Room != nil {
+ r, err := s.getRoom(req.GetRoom())
+ if err != nil {
+ return nil, errRoomNotFound
+ }
+
+ // For substring and whole word, lowercase the keyword.
+ for _, policy := range req.Policies {
+ if policy.Mode == v1.BlacklistMatchMode_BLACKLIST_MATCH_MODE_SUBSTRING || policy.Mode == v1.BlacklistMatchMode_BLACKLIST_MATCH_MODE_WHOLE {
+ policy.Keyword = common.ToLowerUnicode(policy.Keyword)
+ }
+ }
+
+ err = r.Blacklist.AddPolicies(req.Policies)
+ if err != nil {
+ if errors.Is(err, blacklist.ErrEmptyKeyword) {
+ return nil, errEmptyPolicyKeyword
+ }
+
+ return nil, err
+ }
+ } else {
+ // Serverwide policy
+ err := s.s.RoomManager.GlobalBlacklist.AddPolicies(req.Policies)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return &v1.AddBlacklistPoliciesResponse{}, nil
+}
+
+func (s *RpcServer) RemoveBlacklistPolicies(_ context.Context, req *v1.RemoveBlacklistPoliciesRequest) (*v1.RemoveBlacklistPoliciesResponse, error) {
+ // Room scoped policy
+ if req.Room != nil {
+ room, err := s.getRoom(req.GetRoom())
+ if err != nil {
+ return nil, errRoomNotFound
+ }
+
+ err = room.Blacklist.Remove(req.Policies)
+ if err != nil {
+ return nil, err
+ }
+ } else {
+ // Serverwide policy
+ err := s.s.RoomManager.GlobalBlacklist.Remove(req.Policies)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ return &v1.RemoveBlacklistPoliciesResponse{}, nil
+}
+
+func (s *RpcServer) ListBlacklistPolicies(ctx context.Context, req *v1.ListBlacklistPoliciesRequest) (*v1.ListBlacklistPoliciesResponse, error) {
+ r, _ := common.NormalizeRoomName(req.GetRoom())
+ if !r.IsZero() {
+ if _, err := s.getRoom(req.GetRoom()); err != nil {
+ return nil, errRoomNotFound
+ }
+ }
+
+ policies, err := s.s.storage.GetBlacklistPoliciesForRoom(ctx, r)
+ if err != nil {
+ return nil, err
+ }
+
+ return &v1.ListBlacklistPoliciesResponse{
+ Policies: policies,
+ }, nil
+}
diff --git a/server/storage/migration/20260723_serverside_search_blocklist.go b/server/storage/migration/20260723_serverside_search_blocklist.go
new file mode 100644
index 00000000..a408c200
--- /dev/null
+++ b/server/storage/migration/20260723_serverside_search_blocklist.go
@@ -0,0 +1,49 @@
+package migration
+
+import (
+ "database/sql"
+
+ "friendnet.org/common"
+)
+
+type M20260723SearchBlacklist struct {
+}
+
+var _ common.Migration = (*M20260723SearchBlacklist)(nil)
+
+func (m *M20260723SearchBlacklist) Name() string {
+ return "20260723_search_blacklist"
+}
+
+func (m *M20260723SearchBlacklist) Apply(tx *sql.Tx) error {
+ const q = `
+create table search_blacklist
+(
+ word text not null,
+ room text null
+ constraint search_blacklist_room_room_name_fk
+ references room
+ on delete cascade,
+ match_mode integer not null,
+ created_ts integer default (strftime('%s', 'now')) not null,
+
+ primary key (word, room)
+);
+
+create index search_blacklist_room_index on search_blacklist (room);
+create index search_blacklist_room_not_null_index on search_blacklist (room) where room is null;
+create index index_search_blacklist_created_ts_index on search_blacklist (created_ts);
+ `
+
+ _, err := tx.Exec(q)
+ return err
+}
+
+func (m *M20260723SearchBlacklist) Revert(tx *sql.Tx) error {
+ const q = `
+drop table search_blacklist;
+ `
+
+ _, err := tx.Exec(q)
+ return err
+}
diff --git a/server/storage/storage.go b/server/storage/storage.go
index cbc55a44..e8d8a22f 100644
--- a/server/storage/storage.go
+++ b/server/storage/storage.go
@@ -8,6 +8,7 @@ import (
"strings"
"friendnet.org/common"
+ serverrpcv1 "friendnet.org/protocol/pb/serverrpc/v1"
"friendnet.org/server/storage/migration"
_ "modernc.org/sqlite"
)
@@ -55,6 +56,7 @@ func NewStorage(path string) (*Storage, error) {
err = common.DoMigrations(db, []common.Migration{
&migration.M20260208InitialSchema{},
+ &migration.M20260723SearchBlacklist{},
})
if err != nil {
return nil, fmt.Errorf(`failed to apply server database migrations: %w`, err)
@@ -253,3 +255,124 @@ func (s *Storage) DeleteAccountByRoomAndUsername(
}
return nil
}
+
+// AddPoliciesToBlacklist adds blacklist policies for keywords to the persistent blacklist.
+// Assumes the room is either zero or exists (must be verified by the caller).
+// If the room is not specified, the policy is added to the global blacklist.
+func (s *Storage) AddPoliciesToBlacklist(ctx context.Context, room common.NormalizedRoomName, policies []*serverrpcv1.BlacklistPolicy) error {
+ if len(policies) == 0 {
+ return nil
+ }
+
+ tx, err := s.Db.Begin()
+ if err != nil {
+ return err
+ }
+
+ var stmt *sql.Stmt
+ if room.IsZero() {
+ stmt, err = tx.PrepareContext(ctx, `insert into search_blacklist (match_mode, word) values (?, ?)`)
+ defer func() {
+ _ = stmt.Close()
+ }()
+
+ for _, policy := range policies {
+ if _, err := stmt.ExecContext(ctx, policy.GetMode(), policy.Keyword); err != nil {
+ return err
+ }
+ }
+ } else {
+ stmt, err = tx.PrepareContext(ctx, `insert into search_blacklist (room, match_mode, word) values (?, ?, ?)`)
+ defer func() {
+ _ = stmt.Close()
+ }()
+
+ for _, policy := range policies {
+ if _, err := stmt.ExecContext(ctx, room.String(), policy.GetMode(), policy.Keyword); err != nil {
+ return err
+ }
+ }
+ }
+
+ return tx.Commit()
+}
+
+// RemovePoliciesFromBlacklist removes keywords from the persistent blacklist.
+// If room is zero, the keywords are removed from the global blacklist.
+func (s *Storage) RemovePoliciesFromBlacklist(ctx context.Context, room common.NormalizedRoomName, keywords []string) error {
+ if len(keywords) == 0 {
+ return nil
+ }
+
+ tx, err := s.Db.Begin()
+ if err != nil {
+ return err
+ }
+
+ var stmt *sql.Stmt
+ if room.IsZero() {
+ stmt, err = tx.PrepareContext(ctx, `delete from search_blacklist where word = ?`)
+ defer func() {
+ _ = stmt.Close()
+ }()
+
+ for _, keyword := range keywords {
+ if _, err := stmt.ExecContext(ctx, keyword); err != nil {
+ return err
+ }
+ }
+ } else {
+ stmt, err = tx.PrepareContext(ctx, `delete from search_blacklist where room = ? and word = ?`)
+ defer func() {
+ _ = stmt.Close()
+ }()
+
+ for _, keyword := range keywords {
+ if _, err := stmt.ExecContext(ctx, room.String(), keyword); err != nil {
+ return err
+ }
+ }
+ }
+
+ return tx.Commit()
+}
+
+// GetBlacklistPoliciesForRoom will return a list of currently enforced blacklist policies for a given room.
+// If room is zero, it will return the global blacklist.
+// The string searching library necessitates returning a list of rune arrays.
+func (s *Storage) GetBlacklistPoliciesForRoom(ctx context.Context, room common.NormalizedRoomName) ([]*serverrpcv1.BlacklistPolicy, error) {
+ var policies []*serverrpcv1.BlacklistPolicy
+ var rows *sql.Rows
+ var err error
+
+ if room.IsZero() {
+ rows, err = s.Db.QueryContext(ctx, `select match_mode, word from search_blacklist where room is null order by created_ts`)
+ } else {
+ rows, err = s.Db.QueryContext(ctx, `select match_mode, word from search_blacklist where room = ? order by created_ts`, room.String())
+ }
+ if err != nil {
+ return nil, fmt.Errorf(`failed to query rooms: %w`, err)
+ }
+ defer func() {
+ _ = rows.Close()
+ }()
+
+ var matchMode serverrpcv1.BlacklistMatchMode
+ var word string
+ for rows.Next() {
+ if err := rows.Scan(&matchMode, &word); err != nil {
+ return nil, err
+ }
+
+ policies = append(policies, &serverrpcv1.BlacklistPolicy{
+ Keyword: word,
+ Mode: matchMode,
+ })
+ }
+
+ if err := rows.Err(); err != nil {
+ return policies, err
+ }
+
+ return policies, nil
+}
diff --git a/updater/go.mod b/updater/go.mod
index f1ad2d9e..47f18eb2 100644
--- a/updater/go.mod
+++ b/updater/go.mod
@@ -1,3 +1,3 @@
module friendnet.org/updater
-go 1.26.2
+go 1.26.5
diff --git a/upnp/go.mod b/upnp/go.mod
index 137936c9..a07b3d64 100644
--- a/upnp/go.mod
+++ b/upnp/go.mod
@@ -1,3 +1,3 @@
module friendnet.org/upnp
-go 1.26.2
+go 1.26.5
diff --git a/website/docs/server/search-filtering.md b/website/docs/server/search-filtering.md
new file mode 100644
index 00000000..4fe591f8
--- /dev/null
+++ b/website/docs/server/search-filtering.md
@@ -0,0 +1,49 @@
+# Search Filtering
+
+Sometimes, for legal or ethical reasons, you may want to filter out certain search results serverside. It is possible
+to filter out search results for files whose filenames or paths contain blacklisted keywords.
+
+> NOTE: Search filtering only works for serverwide search; it cannot filter direct user searches because those are
+> peer-to-peer.
+
+Filters can be applied globally across the entire server, or on a per-room basis, or a mix of both.
+
+## Filter Match Modes
+
+Keywords can be matches using several modes:
+ - `Substring`
+
+ > Any instance of the word is matched, even if within another word.
+ > For example, `cat` will match to `cats`, `concatenate`, `category`, etc.
+
+ - `Whole Word`
+
+ > Only whole words will be matched.
+ > For example, `stack` will match to `stack`, but not `stackable`, `stacks`, etc.
+
+ - `Regular Expression`
+
+ > Terms matching the specified [regular expression](https://github.com/google/re2/wiki/Syntax) will be matched.
+ > For example, `c[a-z]t` will match to `cat`, `cut`, `cot`, etc.
+ >
+ > Note that regular expressions are case-sensitive.
+
+## Adding From the Admin UI
+
+If you have the [admin UI](setup/management.md) set up, you can add keywords from there.
+
+TODO Screenshot and directions
+
+## Adding From the RPC or CLI
+
+If you are running the server in a terminal or have access to it via the RPC client, you can use that to add keywords.
+
+Type `help` to see all the available blacklist commands.
+
+Currently, these are the commands that exist for managing blacklisted keywords:
+
+ - addglobalblacklistpolicies
+ - addroomblacklistpolicies
+ - removeglobalblacklistpolicies removeglobalblacklistpolicies
+ - removeroomblacklistpolicies
+ - listblacklistpolicies [room]
diff --git a/website/docs/server/toc.txt b/website/docs/server/toc.txt
index 628aff04..a917f41a 100644
--- a/website/docs/server/toc.txt
+++ b/website/docs/server/toc.txt
@@ -2,5 +2,6 @@ setup
rooms
widget
configuring-stun
+search-filtering
troubleshooting
config-validator