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