├── .envrc.recommanded ├── .github └── FUNDING.yml ├── packages ├── icons │ ├── .gitignore │ ├── svgr.config.js │ ├── icons │ │ ├── play.svg │ │ ├── check.svg │ │ ├── chevron-down.svg │ │ ├── chevron-up.svg │ │ ├── x.svg │ │ ├── square.svg │ │ ├── chevron-left.svg │ │ ├── paper-plane.svg │ │ ├── chevron-right.svg │ │ ├── cloud.svg │ │ ├── moon.svg │ │ ├── pause.svg │ │ ├── bold.svg │ │ ├── filter.svg │ │ ├── eye.svg │ │ ├── search.svg │ │ ├── plus.svg │ │ ├── moon-star.svg │ │ ├── archive.svg │ │ ├── bell.svg │ │ ├── filter-x.svg │ │ ├── lock.svg │ │ ├── user.svg │ │ ├── chevrons-down.svg │ │ ├── history.svg │ │ ├── sidebar.svg │ │ ├── panel-left-open.svg │ │ ├── close.svg │ │ ├── corner-down-right.svg │ │ ├── ellipsis-vertical.svg │ │ ├── menu.svg │ │ ├── more-vertical.svg │ │ ├── panel-left-close.svg │ │ ├── key.svg │ │ ├── move-vertical.svg │ │ ├── paperclip.svg │ │ ├── alert-circle.svg │ │ ├── link.svg │ │ ├── user-x.svg │ │ ├── x-circle.svg │ │ ├── arrow-up-wide-short.svg │ │ ├── save.svg │ │ ├── arrow-down-wide-short.svg │ │ ├── triangle-alert.svg │ │ ├── clipboard.svg │ │ ├── cloud-off.svg │ │ ├── edit.svg │ │ ├── satellite-dish.svg │ │ ├── upload.svg │ │ ├── a-arrow-up.svg │ │ ├── log-in.svg │ │ ├── log-out.svg │ │ ├── scaling.svg │ │ ├── alert-triangle.svg │ │ ├── help-circle.svg │ │ ├── external-link.svg │ │ ├── shrink.svg │ │ ├── tool.svg │ │ ├── hash.svg │ │ ├── split-horizontal.svg │ │ ├── globe.svg │ │ ├── refresh.svg │ │ ├── user-plus.svg │ │ ├── thumbs-up.svg │ │ ├── users.svg │ │ ├── thumbs-down.svg │ │ ├── scroll-text.svg │ │ ├── file-plus.svg │ │ ├── mask.svg │ │ ├── book-copy.svg │ │ ├── dice.svg │ │ ├── shuffle.svg │ │ └── trash.svg │ └── tsconfig.json ├── ui │ ├── src │ │ ├── tailwind.css │ │ ├── screens.json │ │ ├── entities │ │ │ ├── EntityExprUnknown.tsx │ │ │ ├── top-level.tsx │ │ │ ├── EntityUnknown.tsx │ │ │ ├── EntityText.tsx │ │ │ ├── EntityExpr.tsx │ │ │ ├── EntityEvaluatedExpr.tsx │ │ │ ├── EntityStrong.tsx │ │ │ └── RollBox.tsx │ │ ├── classes.tsx │ │ ├── chat │ │ │ ├── MessageHandleBox.tsx │ │ │ └── IsActionIndicator.tsx │ │ ├── HelpText.tsx │ │ ├── OopsKaomoji.tsx │ │ ├── BackToHomepage.tsx │ │ ├── users │ │ │ ├── UserCardError.tsx │ │ │ └── UserCardLoading.tsx │ │ ├── ErrorMessageBox.tsx │ │ ├── FallbackIcon.tsx │ │ ├── LampSwitch.tsx │ │ └── Spinner.tsx │ ├── eslint.config.js │ ├── postcss.config.js │ └── tsconfig.json ├── shared-types │ ├── src │ │ └── lib.rs │ └── Cargo.toml ├── types │ ├── locale.ts │ ├── index.ts │ ├── theme.ts │ ├── eslint.config.js │ ├── tsconfig.json │ └── utils.ts ├── utils │ ├── src │ │ ├── string.ts │ │ ├── number.ts │ │ ├── media.ts │ │ ├── errors.ts │ │ ├── id.ts │ │ ├── async.ts │ │ ├── function.ts │ │ ├── files.ts │ │ ├── env.ts │ │ ├── random.ts │ │ └── flags.ts │ ├── eslint.config.js │ └── tsconfig.json ├── eslint-config │ └── README.md ├── backend-proxy │ ├── README.md │ └── package.json ├── store │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── tailwind-config │ ├── postcss.config.js │ ├── fonts │ │ ├── VT323 │ │ │ └── VT323-Regular.ttf │ │ └── fusion-pixel │ │ │ ├── fusion-pixel-10px-monospaced-ja.woff2 │ │ │ ├── fusion-pixel-10px-monospaced-ko.woff2 │ │ │ ├── fusion-pixel-10px-monospaced-latin.woff2 │ │ │ ├── fusion-pixel-10px-monospaced-zh_hans.woff2 │ │ │ └── fusion-pixel-10px-monospaced-zh_hant.woff2 │ └── package.json ├── api │ ├── eslint.config.js │ ├── src │ │ ├── patch.ts │ │ ├── origin-map.ts │ │ ├── events.ts │ │ └── types.ts │ └── tsconfig.json ├── sort │ ├── eslint.config.js │ └── tsconfig.json ├── color │ ├── eslint.config.js │ └── tsconfig.json ├── common │ ├── eslint.config.js │ └── tsconfig.json ├── hooks │ ├── eslint.config.js │ ├── useIsClient.tsx │ ├── tsconfig.json │ ├── useQueryMySpaces.tsx │ ├── useMounted.tsx │ ├── useWebSocketUrl.tsx │ ├── useDetectBrowserSupport.tsx │ └── useQueryChannel.tsx ├── locale │ ├── eslint.config.js │ ├── tsconfig.json │ └── src │ │ └── server.ts ├── settings │ ├── eslint.config.js │ └── tsconfig.json ├── theme │ ├── eslint.config.js │ ├── tsconfig.json │ └── react.ts ├── api-browser │ ├── eslint.config.js │ └── tsconfig.json ├── interpreter │ ├── eslint.config.js │ ├── src │ │ ├── index.ts │ │ └── to-parsed.ts │ └── tsconfig.json └── typescript-config │ ├── react-library.json │ ├── package.json │ └── nextjs.json ├── .rustfmt.toml ├── apps ├── server │ ├── text │ │ ├── email-change │ │ │ ├── title.zh-CN.txt │ │ │ ├── title.zh-TW.txt │ │ │ ├── title.ja.txt │ │ │ ├── title.en.txt │ │ │ ├── content.zh-CN.html │ │ │ ├── content.zh-TW.html │ │ │ ├── content.ja.html │ │ │ └── content.en.html │ │ ├── email-verification │ │ │ ├── title.zh-CN.txt │ │ │ ├── title.zh-TW.txt │ │ │ ├── title.ja.txt │ │ │ ├── title.en.txt │ │ │ ├── content.zh-CN.html │ │ │ ├── content.zh-TW.html │ │ │ ├── content.ja.html │ │ │ └── content.en.html │ │ ├── reset-password │ │ │ ├── title.zh-CN.txt │ │ │ ├── title.zh-TW.txt │ │ │ ├── title.ja.txt │ │ │ ├── title.en.txt │ │ │ ├── content.zh-CN.html │ │ │ ├── content.zh-TW.html │ │ │ ├── content.ja.html │ │ │ └── content.en.html │ │ └── error_serialize_error.json │ ├── sql │ │ ├── users │ │ │ ├── session_revoke.sql │ │ │ ├── session_fetch.sql │ │ │ ├── deactivated.sql │ │ │ ├── get_by_id_list.sql │ │ │ ├── all.sql │ │ │ ├── session_start.sql │ │ │ ├── reset_password.sql │ │ │ ├── get_users_extension.sql │ │ │ ├── remove_avatar.sql │ │ │ ├── create.sql │ │ │ ├── get_by_reset_token.sql │ │ │ ├── reset_token_invalidate.sql │ │ │ ├── reset_token_use.sql │ │ │ ├── set_settings.sql │ │ │ ├── login.sql │ │ │ ├── partial_set_settings.sql │ │ │ ├── edit.sql │ │ │ └── get.sql │ │ ├── spaces │ │ │ ├── delete.sql │ │ │ ├── remove_user_from_space.sql │ │ │ ├── get_settings.sql │ │ │ ├── update_latest_activity.sql │ │ │ ├── get_by_id_list.sql │ │ │ ├── is_public.sql │ │ │ ├── get_token.sql │ │ │ ├── get_by_id.sql │ │ │ ├── recent.sql │ │ │ ├── user_owned_spaces.sql │ │ │ ├── get_by_channel.sql │ │ │ ├── put_settings.sql │ │ │ ├── refresh_token.sql │ │ │ ├── get_space_member.sql │ │ │ ├── search.sql │ │ │ ├── all.sql │ │ │ ├── create.sql │ │ │ ├── get_members_by_channel.sql │ │ │ ├── set_space_member.sql │ │ │ ├── get_members_by_spaces.sql │ │ │ ├── get_space_member_list_by_user.sql │ │ │ ├── get.sql │ │ │ └── edit.sql │ │ ├── media │ │ │ ├── delete.sql │ │ │ ├── get_by_id.sql │ │ │ ├── get_by_filename.sql │ │ │ └── create.sql │ │ ├── messages │ │ │ ├── delete.sql │ │ │ ├── max_pos.sql │ │ │ ├── by_pos.sql │ │ │ ├── set_folded.sql │ │ │ ├── update_space_latest_activity.sql │ │ │ ├── export.sql │ │ │ ├── get_after_pos.sql │ │ │ ├── create.sql │ │ │ ├── get_by_channel.sql │ │ │ ├── move_between.sql │ │ │ ├── edit.sql │ │ │ └── get.sql │ │ └── channels │ │ │ ├── get_by_id_list.sql │ │ │ ├── remove_user_from_channel.sql │ │ │ ├── fetch_channel.sql │ │ │ ├── is_master.sql │ │ │ ├── get_color_list.sql │ │ │ ├── create_channel.sql │ │ │ ├── delete_channel.sql │ │ │ ├── get_by_space.sql │ │ │ ├── get_channel_member_list_by_user.sql │ │ │ ├── get_channel_by_name.sql │ │ │ ├── fetch_channel_with_space.sql │ │ │ ├── remove_user_by_space.sql │ │ │ ├── set_color.sql │ │ │ ├── set_master.sql │ │ │ ├── get_channel_member_list.sql │ │ │ ├── set_name.sql │ │ │ ├── get_channel_member_list_by_user_and_space.sql │ │ │ ├── edit_member.sql │ │ │ ├── get_channels_by_user.sql │ │ │ ├── get_channel_member.sql │ │ │ ├── get_member_by_channel.sql │ │ │ └── get_with_space_member.sql │ ├── src │ │ ├── shutdown.rs │ │ ├── info.rs │ │ ├── config.rs │ │ ├── media.rs │ │ ├── users.rs │ │ ├── messages.rs │ │ ├── spaces.rs │ │ ├── channels.rs │ │ ├── ts.rs │ │ ├── events.rs │ │ └── events │ │ │ └── api.rs │ ├── migrations │ │ ├── 20251001000000_add_channel_archived.sql │ │ └── 20250930021446_remove-messages-foregin-keys.sql │ └── build.rs ├── storybook │ ├── src │ │ ├── vite-env.d.ts │ │ ├── Spinner.stories.ts │ │ ├── LoadFailed.stories.tsx │ │ ├── LoadingText.stories.ts │ │ ├── OopsKaomoji.stories.ts │ │ ├── HelpText.stories.tsx │ │ ├── entities │ │ │ ├── EntityUnknown.stories.tsx │ │ │ └── EntityExprUnknown.stories.tsx │ │ ├── Kbd.stories.ts │ │ ├── SomethingWentWrong.stories.ts │ │ ├── Loading.stories.ts │ │ ├── DiceSelect.stories.ts │ │ ├── ErrorMessageBox.stories.tsx │ │ ├── FloatingBox.stories.ts │ │ ├── BroadcastTurnedOff.stories.tsx │ │ └── IsActionIndicator.stories.tsx │ ├── README.md │ ├── public │ │ └── boluo-pixel.png │ ├── tsconfig.json │ ├── wrangler.toml │ └── vite.config.ts ├── spa │ ├── public │ │ ├── icon.png │ │ ├── favicon.ico │ │ ├── icons │ │ │ ├── app-128px.png │ │ │ ├── app-144px.png │ │ │ ├── app-180px.png │ │ │ ├── app-192px.png │ │ │ ├── app-256px.png │ │ │ └── app-512px.png │ │ └── owlbear-manifest.json │ ├── postcss.config.js │ ├── components │ │ ├── compose │ │ │ └── composeSize.ts │ │ ├── sidebar │ │ │ ├── SidebarGuestContent.tsx │ │ │ ├── ConnectionIndicatorConnecting.tsx │ │ │ ├── SidebarContentLoading.tsx │ │ │ └── SidebarSkeletonItem.tsx │ │ ├── pane-profile │ │ │ └── ShowUsername.tsx │ │ ├── PaneEmpty.tsx │ │ ├── pane-channel │ │ │ └── ChatContentLoading.tsx │ │ ├── pane-channel-settings │ │ │ └── form.ts │ │ ├── PaneFooterBox.tsx │ │ └── ChatNotFound.tsx │ ├── state │ │ ├── dev.atoms.ts │ │ ├── pane-size.ts │ │ ├── unread.atoms.ts │ │ ├── actions.ts │ │ ├── ui.atoms.ts │ │ ├── notification.atoms.ts │ │ └── view.utils.ts │ ├── hooks │ │ ├── useIsInGameChannel.tsx │ │ ├── useDefaultInGame.tsx │ │ ├── usePaneKey.tsx │ │ ├── useDefaultRollCommand.tsx │ │ ├── useIsScrolling.tsx │ │ ├── usePaneIsFocus.tsx │ │ ├── useIsChildPane.tsx │ │ ├── useIsOptimistic.tsx │ │ ├── useIsReordering.tsx │ │ ├── useIsDragging.tsx │ │ ├── useSpace.tsx │ │ ├── useComposeAtom.tsx │ │ ├── useMaxPane.tsx │ │ ├── useBannerNode.tsx │ │ ├── useMember.tsx │ │ ├── useComposeError.tsx │ │ ├── useScrollerRef.tsx │ │ ├── useSettings.tsx │ │ ├── useChannelId.tsx │ │ ├── useReadObserve.tsx │ │ ├── useVirtuosoRef.tsx │ │ ├── useChannel.tsx │ │ └── useChatContainerClassnames.tsx │ ├── eslint.config.js │ ├── pages │ │ ├── layout.tsx │ │ ├── _app.tsx │ │ └── _document.tsx │ ├── sentry.client.config.ts │ ├── production │ │ └── wrangler.toml │ ├── tsconfig.json │ └── wrangler.toml ├── site │ ├── src │ │ ├── app │ │ │ ├── icon.png │ │ │ ├── favicon.ico │ │ │ ├── layout.tsx │ │ │ └── [lang] │ │ │ │ └── [theme] │ │ │ │ └── account │ │ │ │ ├── loading.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── sign-up │ │ │ │ └── Footer.tsx │ │ └── server.ts │ ├── postcss.config.js │ ├── .vercel │ │ ├── README.md │ │ └── project.json │ ├── eslint.config.js │ └── tsconfig.json ├── legacy │ ├── src │ │ ├── assets │ │ │ ├── roll-example.png │ │ │ ├── fusion-pixel.woff2 │ │ │ └── icons │ │ │ │ ├── unfold.svg │ │ │ │ ├── fold.svg │ │ │ │ ├── x.svg │ │ │ │ ├── columns.svg │ │ │ │ ├── rotate-cw.svg │ │ │ │ ├── circle.svg │ │ │ │ ├── uncheck.svg │ │ │ │ ├── x-circle.svg │ │ │ │ ├── lock.svg │ │ │ │ ├── dot-circle.svg │ │ │ │ ├── ellipsis.svg │ │ │ │ ├── filter.svg │ │ │ │ ├── paper-plane.svg │ │ │ │ ├── plus-circle.svg │ │ │ │ ├── chevron-down.svg │ │ │ │ ├── chevron-up.svg │ │ │ │ └── comment-solid.svg │ │ ├── states │ │ │ ├── userDialog.ts │ │ │ └── connection.ts │ │ ├── hooks │ │ │ ├── useIsLoggedIn.ts │ │ │ ├── useMyId.ts │ │ │ ├── useForceUpdate.ts │ │ │ ├── useToggle.ts │ │ │ └── useBaseUrlDelay.ts │ │ ├── components │ │ │ ├── atoms │ │ │ │ ├── Label.tsx │ │ │ │ ├── PanelTitle.tsx │ │ │ │ ├── TextArea.tsx │ │ │ │ ├── Title.tsx │ │ │ │ ├── Separator.tsx │ │ │ │ ├── SpaceGrid.tsx │ │ │ │ ├── News.tsx │ │ │ │ ├── Portal.tsx │ │ │ │ ├── ErrorMessage.tsx │ │ │ │ ├── HelpText.tsx │ │ │ │ ├── Text.tsx │ │ │ │ └── Delay.tsx │ │ │ ├── chat │ │ │ │ ├── ListItemPlaceholder.tsx │ │ │ │ ├── PrivateChat.tsx │ │ │ │ └── compose │ │ │ │ │ └── BroadcastAreClosed.tsx │ │ │ ├── molecules │ │ │ │ └── SpinnerIcon.tsx │ │ │ └── pages │ │ │ │ └── NotFound.tsx │ │ ├── utils │ │ │ ├── profile.ts │ │ │ ├── helper.ts │ │ │ ├── path.ts │ │ │ └── errors.tsx │ │ ├── information.ts │ │ └── api │ │ │ └── media.ts │ └── tsconfig.node.json ├── db │ ├── Dockerfile │ └── README.md ├── theme-designer │ ├── README.md │ └── Cargo.toml └── interpreter-cli │ ├── tsconfig.json │ └── package.json ├── docs └── api │ ├── environments │ ├── Staging.bru │ └── Local.bru │ ├── Info │ ├── folder.bru │ ├── Basic.bru │ └── Health Check.bru │ ├── Events │ ├── folder.bru │ └── Get Token.bru │ ├── Media │ ├── folder.bru │ └── Presigned.bru │ ├── Spaces │ ├── folder.bru │ ├── Get Token.bru │ ├── Members.bru │ ├── Get Settings.bru │ ├── Query Space.bru │ ├── Get Users Status.bru │ ├── My Space Member.bru │ ├── Query With Related.bru │ ├── Delete Space.bru │ ├── Leave Space.bru │ ├── Refresh Token.bru │ ├── Join Space.bru │ ├── Kick Member.bru │ ├── Update Settings.bru │ └── My Spaces.bru │ ├── Users │ ├── folder.bru │ ├── Logout.bru │ ├── Get Settings.bru │ ├── Remove Avatar.bru │ ├── Check Email Verification Status.bru │ ├── Query User.bru │ ├── Check Username.bru │ ├── Check Email.bru │ ├── Query Self.bru │ ├── Get Me.bru │ ├── Resend Email Verification.bru │ ├── Confirm Email Change.bru │ ├── Reset Password.bru │ ├── Request Email Change.bru │ ├── Reset Password Confirm.bru │ ├── Register.bru │ ├── Edit User.bru │ └── Login.bru │ ├── collection.bru │ ├── Channels │ ├── folder.bru │ ├── Members.bru │ ├── All Members.bru │ ├── Query Channel.bru │ ├── Export.bru │ ├── Check Name.bru │ ├── Join Channel.bru │ ├── Leave Channel.bru │ ├── Delete Channel.bru │ ├── Add Member.bru │ ├── Edit Master.bru │ ├── Edit Member.bru │ ├── Kick Member.bru │ └── By Space.bru │ ├── Messages │ ├── folder.bru │ ├── Query Message.bru │ ├── Delete Message.bru │ ├── Toggle Fold.bru │ ├── Move Between.bru │ └── By Channel.bru │ └── bruno.json ├── .env ├── crowdin.yml ├── .gitattributes ├── scripts └── generate-types.sh ├── .vscode └── extensions.json ├── prettier.config.mjs ├── .editorconfig ├── .taplo.toml ├── docker-compose.override.example.yml ├── .sqlx ├── query-ff1f5b18915188e185f0deef5242e09653b3b2bc8623422e549b529b7887cee3.json ├── query-d3323ec6958d941643ea3121a8e35ab44420baf50708e62a3f920e807ec07c1a.json ├── query-0e33a532c76682fe608925c12632c97a3b7362910f02bba15b778f900d650cc0.json ├── query-d30e7b47ad997e47163b89e21fcf7a5ae0a159fb74f492c72d5fbdfa27e344d6.json ├── query-f79ae2c952ff6125556afa20b2f55d44e304fe66877f45d2a61a9f77b1d86b0a.json └── query-da12d4699b27ab86418abc9143bd0eb319f127c73112a0cbe4349213db435632.json └── .env.local.example /.envrc.recommanded: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [uonr] 2 | -------------------------------------------------------------------------------- /packages/icons/.gitignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | match_arm_leading_pipes = "Preserve" 2 | -------------------------------------------------------------------------------- /apps/server/text/email-change/title.zh-CN.txt: -------------------------------------------------------------------------------- 1 | 确认邮箱更改 2 | -------------------------------------------------------------------------------- /apps/server/text/email-change/title.zh-TW.txt: -------------------------------------------------------------------------------- 1 | 確認信箱變更 2 | -------------------------------------------------------------------------------- /apps/server/text/email-change/title.ja.txt: -------------------------------------------------------------------------------- 1 | メールアドレス変更の確認 2 | -------------------------------------------------------------------------------- /apps/server/text/email-verification/title.zh-CN.txt: -------------------------------------------------------------------------------- 1 | 验证您的邮箱地址 2 | -------------------------------------------------------------------------------- /apps/server/text/email-verification/title.zh-TW.txt: -------------------------------------------------------------------------------- 1 | 驗證您的電子郵件地址 2 | -------------------------------------------------------------------------------- /apps/server/text/reset-password/title.zh-CN.txt: -------------------------------------------------------------------------------- 1 | 菠萝RPG - 密码重置邮件 2 | -------------------------------------------------------------------------------- /apps/server/text/reset-password/title.zh-TW.txt: -------------------------------------------------------------------------------- 1 | 菠蘿RPG - 密碼重設郵件 2 | -------------------------------------------------------------------------------- /packages/ui/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @import '@boluo/tailwind-config'; 2 | -------------------------------------------------------------------------------- /apps/server/text/email-change/title.en.txt: -------------------------------------------------------------------------------- 1 | Confirm Your Email Change 2 | -------------------------------------------------------------------------------- /apps/server/text/email-verification/title.ja.txt: -------------------------------------------------------------------------------- 1 | メールアドレスを認証してください 2 | -------------------------------------------------------------------------------- /apps/server/text/reset-password/title.ja.txt: -------------------------------------------------------------------------------- 1 | パインTRPG - パスワードのリセット 2 | -------------------------------------------------------------------------------- /apps/server/text/email-verification/title.en.txt: -------------------------------------------------------------------------------- 1 | Verify Your Email Address 2 | -------------------------------------------------------------------------------- /apps/server/text/reset-password/title.en.txt: -------------------------------------------------------------------------------- 1 | Boluo RPG - Password Reset 2 | -------------------------------------------------------------------------------- /apps/storybook/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/shared-types/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod entities; 2 | pub mod legacy; 3 | -------------------------------------------------------------------------------- /packages/types/locale.ts: -------------------------------------------------------------------------------- 1 | export type Locale = 'en' | 'ja' | 'zh-CN' | 'zh-TW'; 2 | -------------------------------------------------------------------------------- /apps/spa/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/spa/public/icon.png -------------------------------------------------------------------------------- /apps/site/src/app/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/site/src/app/icon.png -------------------------------------------------------------------------------- /apps/spa/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/spa/public/favicon.ico -------------------------------------------------------------------------------- /apps/server/sql/users/session_revoke.sql: -------------------------------------------------------------------------------- 1 | UPDATE user_sessions 2 | SET active = FALSE 3 | WHERE id = $1; 4 | -------------------------------------------------------------------------------- /apps/server/src/shutdown.rs: -------------------------------------------------------------------------------- 1 | pub static SHUTDOWN: tokio::sync::Notify = tokio::sync::Notify::const_new(); 2 | -------------------------------------------------------------------------------- /apps/site/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/site/src/app/favicon.ico -------------------------------------------------------------------------------- /apps/storybook/README.md: -------------------------------------------------------------------------------- 1 | # Storybook 2 | 3 | - [Storybook Documentation](https://storybook.js.org/docs) 4 | -------------------------------------------------------------------------------- /docs/api/environments/Staging.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | base_url: https://boluo-server-staging.fly.dev/api 3 | } 4 | -------------------------------------------------------------------------------- /packages/utils/src/string.ts: -------------------------------------------------------------------------------- 1 | export const splitByLine = (text: string): string[] => text.split(/\r?\n/); 2 | -------------------------------------------------------------------------------- /docs/api/Info/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Info 3 | seq: 3 4 | } 5 | 6 | auth { 7 | mode: inherit 8 | } 9 | -------------------------------------------------------------------------------- /packages/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './locale'; 2 | export * from './theme'; 3 | export * from './utils'; 4 | -------------------------------------------------------------------------------- /apps/spa/public/icons/app-128px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/spa/public/icons/app-128px.png -------------------------------------------------------------------------------- /apps/spa/public/icons/app-144px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/spa/public/icons/app-144px.png -------------------------------------------------------------------------------- /apps/spa/public/icons/app-180px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/spa/public/icons/app-180px.png -------------------------------------------------------------------------------- /apps/spa/public/icons/app-192px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/spa/public/icons/app-192px.png -------------------------------------------------------------------------------- /apps/spa/public/icons/app-256px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/spa/public/icons/app-256px.png -------------------------------------------------------------------------------- /apps/spa/public/icons/app-512px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/spa/public/icons/app-512px.png -------------------------------------------------------------------------------- /docs/api/Events/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Events 3 | seq: 7 4 | } 5 | 6 | auth { 7 | mode: inherit 8 | } 9 | -------------------------------------------------------------------------------- /docs/api/Media/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Media 3 | seq: 8 4 | } 5 | 6 | auth { 7 | mode: inherit 8 | } 9 | -------------------------------------------------------------------------------- /docs/api/Spaces/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Spaces 3 | seq: 4 4 | } 5 | 6 | auth { 7 | mode: inherit 8 | } 9 | -------------------------------------------------------------------------------- /docs/api/Users/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Users 3 | seq: 1 4 | } 5 | 6 | auth { 7 | mode: inherit 8 | } 9 | -------------------------------------------------------------------------------- /docs/api/collection.bru: -------------------------------------------------------------------------------- 1 | auth { 2 | mode: bearer 3 | } 4 | 5 | auth:bearer { 6 | token: {{access_token}} 7 | } 8 | -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/delete.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | spaces 3 | SET 4 | deleted = TRUE 5 | WHERE 6 | id = $1; 7 | 8 | -------------------------------------------------------------------------------- /apps/server/src/info.rs: -------------------------------------------------------------------------------- 1 | mod handlers; 2 | mod models; 3 | 4 | pub use handlers::router; 5 | pub use models::BasicInfo; 6 | -------------------------------------------------------------------------------- /apps/storybook/public/boluo-pixel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/storybook/public/boluo-pixel.png -------------------------------------------------------------------------------- /docs/api/Channels/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Channels 3 | seq: 5 4 | } 5 | 6 | auth { 7 | mode: inherit 8 | } 9 | -------------------------------------------------------------------------------- /docs/api/Messages/folder.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Messages 3 | seq: 6 4 | } 5 | 6 | auth { 7 | mode: inherit 8 | } 9 | -------------------------------------------------------------------------------- /packages/ui/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/react-internal'; 2 | export default config; 3 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/roll-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/legacy/src/assets/roll-example.png -------------------------------------------------------------------------------- /apps/server/sql/media/delete.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM media 2 | WHERE id = $1 3 | RETURNING 4 | media AS "media!: Media"; 5 | 6 | -------------------------------------------------------------------------------- /apps/server/sql/messages/delete.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | messages 3 | SET 4 | deleted = TRUE 5 | WHERE 6 | id = $1; 7 | 8 | -------------------------------------------------------------------------------- /apps/server/sql/users/session_fetch.sql: -------------------------------------------------------------------------------- 1 | SELECT id, user_id, created FROM user_sessions 2 | WHERE id = $1 AND active = TRUE; 3 | -------------------------------------------------------------------------------- /packages/backend-proxy/README.md: -------------------------------------------------------------------------------- 1 | # Backend Proxy 2 | 3 | A cloudflare worker that proxies requests to a backend service. 4 | -------------------------------------------------------------------------------- /packages/ui/src/screens.json: -------------------------------------------------------------------------------- 1 | { 2 | "sm": 640, 3 | "md": 768, 4 | "lg": 1024, 5 | "xl": 1280, 6 | "2xl": 1536 7 | } 8 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/fusion-pixel.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/apps/legacy/src/assets/fusion-pixel.woff2 -------------------------------------------------------------------------------- /apps/server/sql/users/deactivated.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | users 3 | SET 4 | deactivated = TRUE 5 | WHERE 6 | id = $1; 7 | 8 | -------------------------------------------------------------------------------- /apps/server/sql/users/get_by_id_list.sql: -------------------------------------------------------------------------------- 1 | SELECT users as "users!: User" FROM users WHERE id = ANY($1) AND deactivated = false 2 | -------------------------------------------------------------------------------- /apps/spa/postcss.config.js: -------------------------------------------------------------------------------- 1 | import { postcssConfig } from '@boluo/tailwind-config/postcss'; 2 | 3 | export default postcssConfig; 4 | -------------------------------------------------------------------------------- /docs/api/environments/Local.bru: -------------------------------------------------------------------------------- 1 | vars { 2 | base_url: http://127.0.0.1:3033/api 3 | } 4 | vars:secret [ 5 | access_token 6 | ] 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | MINIO_ROOT_USER=boluo 2 | MINIO_ROOT_PASSWORD=boluo-development 3 | PORT=3033 4 | RUST_LOG=debug 5 | NEXT_TELEMETRY_DISABLED=1 6 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/remove_user_from_space.sql: -------------------------------------------------------------------------------- 1 | DELETE FROM space_members 2 | WHERE user_id = $1 3 | AND space_id = $2; 4 | 5 | -------------------------------------------------------------------------------- /apps/site/postcss.config.js: -------------------------------------------------------------------------------- 1 | import { postcssConfig } from '@boluo/tailwind-config/postcss'; 2 | 3 | export default postcssConfig; 4 | -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /packages/lang/src/en.json 3 | translation: /packages/lang/src/%locale_with_underscore%.json 4 | -------------------------------------------------------------------------------- /packages/ui/postcss.config.js: -------------------------------------------------------------------------------- 1 | import { postcssConfig } from '@boluo/tailwind-config/postcss'; 2 | 3 | export default postcssConfig; 4 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_settings.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | settings 3 | FROM 4 | spaces_extension 5 | WHERE 6 | space_id = $1; 7 | 8 | -------------------------------------------------------------------------------- /apps/server/sql/users/all.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | users AS "users!: User" 3 | FROM 4 | users 5 | WHERE 6 | deactivated = FALSE; 7 | 8 | -------------------------------------------------------------------------------- /apps/server/sql/users/session_start.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO user_sessions (id, user_id) 2 | VALUES ($1, $2) 3 | RETURNING id, user_id, created; 4 | -------------------------------------------------------------------------------- /apps/server/src/config.rs: -------------------------------------------------------------------------------- 1 | pub(crate) fn load() { 2 | dotenvy::from_filename(".env.local").ok(); 3 | dotenvy::dotenv().ok(); 4 | } 5 | -------------------------------------------------------------------------------- /apps/spa/components/compose/composeSize.ts: -------------------------------------------------------------------------------- 1 | export const COMPOSE_LARGE_HEIGHT = '40vh'; 2 | export const COMPOSE_AUTO_MAX_HEIGHT = '12rem'; 3 | -------------------------------------------------------------------------------- /packages/utils/src/number.ts: -------------------------------------------------------------------------------- 1 | export const clamp = (value: number, min: number, max: number) => 2 | Math.max(min, Math.min(max, value)); 3 | -------------------------------------------------------------------------------- /apps/server/sql/media/get_by_id.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | media AS "media!: Media" 3 | FROM 4 | media 5 | WHERE 6 | id = $1 7 | LIMIT 1; 8 | 9 | -------------------------------------------------------------------------------- /apps/spa/state/dev.atoms.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from 'jotai/utils'; 2 | 3 | export const devMode = atomWithStorage('dev-mode', false); 4 | -------------------------------------------------------------------------------- /packages/store/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from 'jotai'; 2 | 3 | export const store: ReturnType = createStore(); 4 | -------------------------------------------------------------------------------- /packages/tailwind-config/postcss.config.js: -------------------------------------------------------------------------------- 1 | export const postcssConfig = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /apps/server/sql/users/reset_password.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | users 3 | SET 4 | PASSWORD = crypt($2, gen_salt('bf')) 5 | WHERE 6 | id = $1; 7 | 8 | -------------------------------------------------------------------------------- /apps/server/src/media.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod api; 2 | mod handlers; 3 | pub(crate) mod models; 4 | 5 | pub use handlers::{router, upload, upload_params}; 6 | -------------------------------------------------------------------------------- /apps/server/sql/media/get_by_filename.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | media AS "media!: Media" 3 | FROM 4 | media 5 | WHERE 6 | filename = $1 7 | LIMIT 1; 8 | 9 | -------------------------------------------------------------------------------- /packages/tailwind-config/fonts/VT323/VT323-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/VT323/VT323-Regular.ttf -------------------------------------------------------------------------------- /packages/ui/src/entities/EntityExprUnknown.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | 3 | export const EntityExprNodeUnknown: FC = () => ???; 4 | -------------------------------------------------------------------------------- /packages/types/theme.ts: -------------------------------------------------------------------------------- 1 | export type Theme = 'light' | 'dark' | 'graphite' | 'dusha' | 'system'; 2 | 3 | export type ResolvedTheme = Exclude; 4 | -------------------------------------------------------------------------------- /packages/utils/src/media.ts: -------------------------------------------------------------------------------- 1 | export function getMediaUrl(mediaPublicUrl: string, mediaId: string): string { 2 | return `${mediaPublicUrl}/${mediaId}`; 3 | } 4 | -------------------------------------------------------------------------------- /apps/server/migrations/20251001000000_add_channel_archived.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE channels 2 | ADD COLUMN IF NOT EXISTS is_archived boolean NOT NULL DEFAULT FALSE; 3 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/update_latest_activity.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | spaces 3 | SET 4 | latest_activity = now() at time zone 'utc' 5 | WHERE 6 | id = $1; 7 | 8 | -------------------------------------------------------------------------------- /apps/server/src/users.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod handlers; 3 | mod models; 4 | 5 | pub use api::GetMe; 6 | pub use handlers::router; 7 | pub use models::{User, UserExt}; 8 | -------------------------------------------------------------------------------- /apps/site/.vercel/README.md: -------------------------------------------------------------------------------- 1 | Despite the name, this is configured for cloudflare pages. 2 | 3 | See [next-on-pages](https://github.com/cloudflare/next-on-pages) 4 | -------------------------------------------------------------------------------- /apps/spa/hooks/useIsInGameChannel.tsx: -------------------------------------------------------------------------------- 1 | import { useChannel } from './useChannel'; 2 | 3 | export const useIsInGameChannel = () => useChannel()?.type === 'IN_GAME'; 4 | -------------------------------------------------------------------------------- /docs/api/bruno.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "boluo-api", 4 | "type": "collection", 5 | "ignore": [ 6 | "node_modules", 7 | ".git" 8 | ] 9 | } -------------------------------------------------------------------------------- /packages/api/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/sort/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_by_id_list.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | s AS "space!: Space" 3 | FROM 4 | spaces s 5 | WHERE 6 | s.id = ANY($1) 7 | AND deleted = FALSE; 8 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/is_public.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | is_public 3 | FROM 4 | spaces 5 | WHERE 6 | id = $1 7 | AND deleted = FALSE 8 | LIMIT 1; 9 | 10 | -------------------------------------------------------------------------------- /packages/color/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/common/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/hooks/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/locale/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/settings/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/theme/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/types/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/utils/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /apps/legacy/src/states/userDialog.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { type Id } from '../utils/id'; 3 | 4 | export const userDialogAtom = atom(null); 5 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_token.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | invite_token 3 | FROM 4 | spaces 5 | WHERE 6 | id = $1 7 | AND deleted = FALSE 8 | LIMIT 1; 9 | 10 | -------------------------------------------------------------------------------- /apps/server/sql/users/get_users_extension.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | users_extension AS "user_ext!: UserExt" 3 | FROM 4 | users_extension 5 | WHERE 6 | user_id = $1; 7 | 8 | -------------------------------------------------------------------------------- /apps/server/text/reset-password/content.zh-CN.html: -------------------------------------------------------------------------------- 1 |

2 | 你请求了重置密码。 3 | 点这里进行重置。 4 |

5 | 6 |

如果并没有请求重置,请忽略这封邮件

7 | -------------------------------------------------------------------------------- /apps/server/text/reset-password/content.zh-TW.html: -------------------------------------------------------------------------------- 1 |

2 | 你請求了重設密碼。 3 | 點這裡進行重設。 4 |

5 | 6 |

如果並沒有請求重設,請忽略這封郵件

7 | -------------------------------------------------------------------------------- /apps/spa/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { nextJsConfig } from '@boluo/eslint-config/next-js'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default nextJsConfig; 5 | -------------------------------------------------------------------------------- /apps/storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { "path": "./tsconfig.app.json" }, 5 | { "path": "./tsconfig.node.json" } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /packages/api-browser/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /packages/interpreter/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { config } from '@boluo/eslint-config/base'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_by_id_list.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | ch AS "channel!: Channel" 3 | FROM 4 | channels ch 5 | WHERE 6 | ch.id = ANY($1) 7 | AND deleted = FALSE; 8 | -------------------------------------------------------------------------------- /apps/server/src/messages.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | mod handlers; 3 | mod models; 4 | 5 | pub use handlers::router; 6 | pub use models::Entities; 7 | pub use models::{MaxPos, Message}; 8 | -------------------------------------------------------------------------------- /apps/site/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { nextJsConfig } from '@boluo/eslint-config/next-js'; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default nextJsConfig; 5 | -------------------------------------------------------------------------------- /apps/spa/pages/layout.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from 'react'; 2 | 3 | export default function Layout({ children }: { children: ReactNode }) { 4 | return children; 5 | } 6 | -------------------------------------------------------------------------------- /apps/server/sql/channels/remove_user_from_channel.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | channel_members 3 | SET 4 | is_joined = FALSE 5 | WHERE 6 | user_id = $1 7 | AND channel_id = $2; 8 | 9 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_by_id.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | s AS "space!: Space" 3 | FROM 4 | spaces s 5 | WHERE 6 | s.id = $1 7 | AND deleted = FALSE 8 | LIMIT 1; 9 | 10 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/recent.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | id AS "id!: Uuid" 3 | FROM 4 | spaces 5 | WHERE 6 | deleted = FALSE 7 | AND latest_activity > now() - interval '2 hours'; 8 | -------------------------------------------------------------------------------- /apps/server/sql/users/remove_avatar.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | users 3 | SET 4 | avatar_id = NULL 5 | WHERE 6 | id = $1 7 | RETURNING 8 | users AS "users!: User"; 9 | 10 | -------------------------------------------------------------------------------- /apps/server/src/spaces.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod handlers; 3 | pub mod models; 4 | 5 | pub use handlers::router; 6 | pub use models::{Space, SpaceMember, SpaceSettings, UserSpaces}; 7 | -------------------------------------------------------------------------------- /docs/api/Info/Basic.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Basic 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/info 9 | body: none 10 | auth: inherit 11 | } 12 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/user_owned_spaces.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | spaces AS "space!: Space" 3 | FROM 4 | spaces 5 | WHERE 6 | spaces.owner_id = $1 7 | AND spaces.deleted = FALSE; 8 | 9 | -------------------------------------------------------------------------------- /packages/interpreter/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './eval'; 2 | export * from './entities'; 3 | export * from './parse-result'; 4 | export * from './parser'; 5 | export * from './to-parsed'; 6 | -------------------------------------------------------------------------------- /packages/utils/src/errors.ts: -------------------------------------------------------------------------------- 1 | export const isChunkLoadError = (error: unknown): error is Error => { 2 | return error instanceof Error && error.message.includes('Loading chunk'); 3 | }; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # https://github.com/github-linguist/linguist/blob/master/docs/overrides.md#vendored-code 2 | packages/api/src/bindings.ts linguist-generated 3 | **/.sqlx/* linguist-generated 4 | -------------------------------------------------------------------------------- /apps/db/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM postgres:18-trixie 2 | COPY postgresql.conf /etc/postgresql/postgresql.conf 3 | 4 | CMD ["postgres", "-c", "config_file=/etc/postgresql/postgresql.conf"] 5 | EXPOSE 5432 6 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_by_channel.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | s AS "space!: Space" 3 | FROM 4 | channels ch 5 | INNER JOIN spaces s ON ch.space_id = s.id 6 | WHERE 7 | ch.id = $1; 8 | 9 | -------------------------------------------------------------------------------- /apps/server/src/channels.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | pub mod handlers; 3 | pub mod models; 4 | 5 | pub use handlers::router; 6 | pub use models::{Channel, ChannelMember, ChannelMembers, ChannelType}; 7 | -------------------------------------------------------------------------------- /docs/api/Users/Logout.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Logout 3 | type: http 4 | seq: 6 5 | } 6 | 7 | get { 8 | url: {{base_url}}/users/logout 9 | body: none 10 | auth: inherit 11 | } 12 | -------------------------------------------------------------------------------- /apps/server/sql/channels/fetch_channel.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | ch AS "channel!: Channel" 3 | FROM 4 | channels ch 5 | WHERE 6 | ch.id = $1 7 | AND deleted = FALSE 8 | LIMIT 1; 9 | 10 | -------------------------------------------------------------------------------- /apps/server/sql/users/create.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO users (email, username, nickname, PASSWORD) 2 | VALUES ($1, $2, $3, crypt($4, gen_salt('bf'))) 3 | RETURNING 4 | users AS "users!: User"; 5 | 6 | -------------------------------------------------------------------------------- /apps/server/sql/users/get_by_reset_token.sql: -------------------------------------------------------------------------------- 1 | SELECT users AS "users!: User" 2 | FROM users INNER JOIN reset_tokens ON users.id = reset_tokens.user_id 3 | WHERE reset_tokens.token = $1 4 | LIMIT 1; 5 | -------------------------------------------------------------------------------- /scripts/generate-types.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | cargo run -p server -- --types 4 | 5 | npm exec prettier -- --write ./packages/types/bindings.ts 6 | cargo sqlx prepare --workspace -- --tests 7 | -------------------------------------------------------------------------------- /apps/legacy/src/hooks/useIsLoggedIn.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from '../store'; 2 | 3 | export function useIsLoggedIn(): boolean { 4 | return useSelector((state) => state.profile !== undefined); 5 | } 6 | -------------------------------------------------------------------------------- /apps/storybook/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "boluo-storybook" 2 | compatibility_date = "2025-07-25" 3 | 4 | [assets] 5 | directory = "./storybook-static/" 6 | not_found_handling = "single-page-application" 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "bradlc.vscode-tailwindcss", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/put_settings.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO spaces_extension (space_id, settings) 2 | VALUES ($1, $2) 3 | ON CONFLICT (space_id) 4 | DO UPDATE SET 5 | settings = EXCLUDED.settings; 6 | 7 | -------------------------------------------------------------------------------- /docs/api/Info/Health Check.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Health Check 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{base_url}}/info/healthcheck 9 | body: none 10 | auth: inherit 11 | } 12 | -------------------------------------------------------------------------------- /docs/api/Users/Get Settings.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get Settings 3 | type: http 4 | seq: 7 5 | } 6 | 7 | get { 8 | url: {{base_url}}/users/settings 9 | body: none 10 | auth: inherit 11 | } 12 | -------------------------------------------------------------------------------- /packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-ja.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-ja.woff2 -------------------------------------------------------------------------------- /packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-ko.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-ko.woff2 -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/server/sql/messages/max_pos.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | pos_p, 3 | pos_q, 4 | id 5 | FROM 6 | messages msg 7 | WHERE 8 | channel_id = $1 9 | ORDER BY 10 | msg.pos DESC 11 | LIMIT 1; 12 | 13 | -------------------------------------------------------------------------------- /packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-latin.woff2 -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/Label.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { block, pY } from '../../styles/atoms'; 3 | 4 | export const Label = styled.label` 5 | ${pY(2)}; 6 | ${block}; 7 | `; 8 | -------------------------------------------------------------------------------- /apps/server/sql/channels/is_master.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | is_master 3 | FROM 4 | channel_members cm 5 | WHERE 6 | cm.user_id = $1 7 | AND cm.channel_id = $2 8 | AND cm.is_joined 9 | LIMIT 1; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/refresh_token.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | spaces 3 | SET 4 | invite_token = gen_random_uuid () 5 | WHERE 6 | id = $1 7 | AND deleted = FALSE 8 | RETURNING 9 | invite_token; 10 | 11 | -------------------------------------------------------------------------------- /packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-zh_hans.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-zh_hans.woff2 -------------------------------------------------------------------------------- /packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-zh_hant.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mythal/boluo/HEAD/packages/tailwind-config/fonts/fusion-pixel/fusion-pixel-10px-monospaced-zh_hant.woff2 -------------------------------------------------------------------------------- /packages/ui/src/classes.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export const link = clsx( 4 | 'text-text-link hover:text-text-link-hover active:text-text-link-active underline decoration-text-link-decoration', 5 | ); 6 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import("prettier").Config} */ 2 | const config = { 3 | plugins: ['prettier-plugin-tailwindcss'], 4 | singleQuote: true, 5 | printWidth: 100, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_space_member.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | member AS "space_member!: SpaceMember" 3 | FROM 4 | space_members member 5 | WHERE 6 | user_id = $1 7 | AND space_id = $2 8 | LIMIT 1; 9 | 10 | -------------------------------------------------------------------------------- /apps/server/sql/users/reset_token_invalidate.sql: -------------------------------------------------------------------------------- 1 | UPDATE reset_tokens 2 | SET 3 | invalidated_at = now() at time zone 'utc' 4 | WHERE 5 | user_id = $1 6 | AND used_at IS NULL 7 | AND invalidated_at IS NULL; 8 | -------------------------------------------------------------------------------- /apps/spa/hooks/useDefaultInGame.tsx: -------------------------------------------------------------------------------- 1 | import { useChannel } from './useChannel'; 2 | 3 | export const useDefaultInGame = (): boolean => { 4 | const channel = useChannel(); 5 | return channel?.type === 'IN_GAME'; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_color_list.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | user_id, 3 | text_color AS "color!" 4 | FROM 5 | channel_members cm 6 | WHERE 7 | cm.channel_id = $1 8 | AND cm.text_color IS NOT NULL; 9 | 10 | -------------------------------------------------------------------------------- /apps/server/sql/media/create.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO media (id, mime_type, uploader_id, filename, original_filename, hash, size, source) 2 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8) 3 | RETURNING 4 | media AS "media!: Media"; 5 | 6 | -------------------------------------------------------------------------------- /apps/server/sql/messages/by_pos.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | msg AS "message!: Message" 3 | FROM 4 | messages msg 5 | WHERE 6 | msg.channel_id = $1 7 | AND msg.pos_p = $2 8 | AND msg.pos_q = $3 9 | LIMIT 1; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/search.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | spaces AS "space!: Space" 3 | FROM 4 | spaces 5 | WHERE 6 | deleted = FALSE 7 | AND concat(name, ' ', description) 8 | LIKE ALL ($1) 9 | LIMIT 1024; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/text/error_serialize_error.json: -------------------------------------------------------------------------------- 1 | { 2 | "is_ok": false, 3 | "ok": null, 4 | "err": { 5 | "code": "UNEXPECTED", 6 | "message": "Unexpected serialize error", 7 | "table": null 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/site/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import '@boluo/tailwind-config'; 3 | 4 | export default function RootLayout({ children }: { children: ReactNode }) { 5 | return <>{children}; 6 | } 7 | -------------------------------------------------------------------------------- /apps/spa/components/sidebar/SidebarGuestContent.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | 3 | export const SidebarGuestContent: FC = () => { 4 | return
{/* Guest content can be added here in the future */}
; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/spa/hooks/usePaneKey.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { PaneContext } from '../state/view.context'; 3 | 4 | export const usePaneKey = (): number | null => { 5 | return useContext(PaneContext).key; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/ui/src/chat/MessageHandleBox.tsx: -------------------------------------------------------------------------------- 1 | export const MessageHandleBox = ({ children }: { children: React.ReactNode }) => { 2 | return
{children}
; 3 | }; 4 | -------------------------------------------------------------------------------- /apps/server/sql/channels/create_channel.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO channels (space_id, name, is_public, default_dice_type, "type") 2 | VALUES ($1, $2, $3, COALESCE($4, 'd20'), $5) 3 | RETURNING 4 | channels AS "channel!: Channel"; 5 | 6 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/all.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | spaces AS "space!: Space" 3 | FROM 4 | spaces 5 | WHERE 6 | deleted = FALSE 7 | AND explorable = TRUE 8 | ORDER BY 9 | latest_activity DESC 10 | LIMIT 512; 11 | 12 | -------------------------------------------------------------------------------- /apps/server/sql/users/reset_token_use.sql: -------------------------------------------------------------------------------- 1 | UPDATE reset_tokens 2 | SET 3 | used_at = now() at time zone 'utc' 4 | WHERE 5 | token = $1 6 | AND user_id = $2 7 | AND used_at IS NULL 8 | AND invalidated_at IS NULL; 9 | -------------------------------------------------------------------------------- /apps/server/text/email-verification/content.zh-CN.html: -------------------------------------------------------------------------------- 1 |

您好!

2 |

感谢您注册 Boluo。请点击下面的链接来验证您的邮箱地址:

3 |

验证邮箱

4 |

此链接将在24小时后过期。

5 |

如果您没有注册 Boluo 账户,请忽略此邮件。

6 | -------------------------------------------------------------------------------- /apps/spa/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@boluo/tailwind-config'; 2 | 3 | import type { AppProps } from 'next/app'; 4 | 5 | export default function SpaApp({ Component, pageProps }: AppProps) { 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@boluo/typescript-config", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": true, 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/utils/src/id.ts: -------------------------------------------------------------------------------- 1 | import { v1 as makeId, validate } from 'uuid'; 2 | 3 | export type Id = string; 4 | 5 | export const isUuid = (x: unknown): x is string => typeof x === 'string' && validate(x); 6 | 7 | export { makeId }; 8 | -------------------------------------------------------------------------------- /apps/legacy/src/hooks/useMyId.ts: -------------------------------------------------------------------------------- 1 | import { useSelector } from '../store'; 2 | import { type Id } from '../utils/id'; 3 | 4 | export const useMyId = (): Id | undefined => { 5 | return useSelector((state) => state.profile?.user.id); 6 | }; 7 | -------------------------------------------------------------------------------- /apps/server/sql/channels/delete_channel.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | channels 3 | SET 4 | deleted = TRUE, 5 | old_name = name, 6 | name = uuid_generate_v4 ()::text 7 | WHERE 8 | id = $1 9 | AND deleted = FALSE; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/sql/messages/set_folded.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | messages 3 | SET 4 | folded = $2, 5 | modified = (now() at time zone 'utc') 6 | WHERE 7 | id = $1 8 | RETURNING 9 | messages AS "message!: Message"; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/sql/messages/update_space_latest_activity.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | spaces 3 | SET 4 | latest_activity = $2 5 | FROM 6 | channels 7 | WHERE 8 | spaces.id = channels.space_id 9 | AND channels.id = $1; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/create.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO spaces ("name", owner_id, "password", default_dice_type, "description") 2 | VALUES ($1, $2, COALESCE($3, ''), COALESCE($4, 'd20'), $5) 3 | RETURNING 4 | spaces AS "space!: Space"; 5 | 6 | -------------------------------------------------------------------------------- /apps/server/text/email-verification/content.zh-TW.html: -------------------------------------------------------------------------------- 1 |

您好!

2 |

感謝您註冊 Boluo。請點擊下面的連結來驗證您的電子郵件地址:

3 |

驗證電子郵件

4 |

此連結將在24小時後過期。

5 |

如果您沒有註冊 Boluo 帳戶,請忽略此郵件。

6 | -------------------------------------------------------------------------------- /apps/spa/hooks/useDefaultRollCommand.tsx: -------------------------------------------------------------------------------- 1 | import { useChannel } from './useChannel'; 2 | 3 | export const useDefaultRollCommand = (): string => { 4 | const channel = useChannel(); 5 | return channel?.defaultRollCommand || 'd'; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/spa/hooks/useIsScrolling.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export const IsScrollingContext = createContext(false); 4 | 5 | export const useIsScrolling = (): boolean => useContext(IsScrollingContext); 6 | -------------------------------------------------------------------------------- /apps/spa/hooks/usePaneIsFocus.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { PaneContext } from '../state/view.context'; 3 | 4 | export const usePaneIsFocus = (): boolean => { 5 | return useContext(PaneContext).focused; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/spa/state/pane-size.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import React from 'react'; 3 | 4 | const sizeLevelAtom = atom(0); 5 | 6 | export const SizeLevelContext = React.createContext(sizeLevelAtom); 7 | -------------------------------------------------------------------------------- /packages/store/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist/" 5 | }, 6 | "include": ["src/**/*.ts"], 7 | "exclude": ["dist", "build", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_by_space.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | channel AS "channel!: Channel" 3 | FROM 4 | channels channel 5 | WHERE 6 | channel.space_id = $1 7 | AND deleted = FALSE 8 | ORDER BY 9 | channel.created; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/text/reset-password/content.ja.html: -------------------------------------------------------------------------------- 1 |

2 | パスワードのリセットをリクエストしました。 パスワードをリセットするには、ここをクリックしてください。 6 |

7 | 8 |

パスワードのリセットをリクエストしていない場合は、このメールを無視してください。

9 | -------------------------------------------------------------------------------- /apps/spa/state/unread.atoms.ts: -------------------------------------------------------------------------------- 1 | import { atomFamily, atomWithStorage } from 'jotai/utils'; 2 | 3 | export const channelReadFamily = atomFamily((channelId: string) => 4 | atomWithStorage(`channel:${channelId}:read`, Number.MIN_VALUE), 5 | ); 6 | -------------------------------------------------------------------------------- /packages/ui/src/entities/top-level.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const IsTopLevelContext = React.createContext(true); 4 | 5 | export function useIsTopLevel() { 6 | return React.useContext(IsTopLevelContext); 7 | } 8 | -------------------------------------------------------------------------------- /apps/spa/components/pane-profile/ShowUsername.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | 3 | export const ShowUsername: FC<{ username: string }> = ({ username }) => { 4 | return
{username}
; 5 | }; 6 | -------------------------------------------------------------------------------- /apps/spa/hooks/useIsChildPane.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export const IsChildPaneContext = createContext(false); 4 | 5 | export const useIsChildPane = () => { 6 | return useContext(IsChildPaneContext); 7 | }; 8 | -------------------------------------------------------------------------------- /apps/theme-designer/README.md: -------------------------------------------------------------------------------- 1 | # Theme Designer 2 | 3 | A GUI tool to help design themes for the application. 4 | 5 | ## Prerequisites 6 | 7 | - ripgrep 8 | 9 | ## Running 10 | 11 | ``` 12 | cargo run --package theme-designer 13 | ``` 14 | -------------------------------------------------------------------------------- /apps/server/text/email-change/content.zh-CN.html: -------------------------------------------------------------------------------- 1 |

您好!

2 |

您已请求更改您的 Boluo 账户邮箱地址。请点击下面的链接确认此更改:

3 |

确认邮箱更改

4 |

此链接将在24小时后失效。

5 |

如果您没有请求更改邮箱,请忽略此邮件,您当前的邮箱地址将保持不变。

6 | -------------------------------------------------------------------------------- /apps/server/text/email-change/content.zh-TW.html: -------------------------------------------------------------------------------- 1 |

您好!

2 |

您已請求變更您的 Boluo 帳戶信箱地址。請點擊下面的連結確認此變更:

3 |

確認信箱變更

4 |

此連結將在24小時後失效。

5 |

如果您沒有請求變更信箱,請忽略此郵件,您目前的信箱地址將保持不變。

6 | -------------------------------------------------------------------------------- /apps/spa/hooks/useIsOptimistic.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export const IsOptimisticContext = createContext(false); 4 | 5 | export const useIsOptimistic = () => { 6 | return useContext(IsOptimisticContext); 7 | }; 8 | -------------------------------------------------------------------------------- /apps/spa/hooks/useIsReordering.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export const IsReorderingContext = createContext(false); 4 | 5 | export const useIsReordering = () => { 6 | return useContext(IsReorderingContext); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/hooks/useIsClient.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useIsClient = () => { 4 | const [isClient, setIsClient] = useState(false); 5 | useEffect(() => setIsClient(true), []); 6 | return isClient; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/icons/svgr.config.js: -------------------------------------------------------------------------------- 1 | // see https://react-svgr.com/docs/options/ 2 | module.exports = { 3 | typescript: true, 4 | outDir: './src/', 5 | jsxRuntime: 'automatic', 6 | icon: '1em', 7 | memo: false, 8 | prettier: false, 9 | }; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | [*] 3 | end_of_line = lf 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | charset = utf-8 7 | 8 | [*.{js,jsx,mjs,cjs,ts,tsx,json,css,webmanifest}] 9 | indent_style = space 10 | indent_size = 2 11 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/PanelTitle.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { fontNormal, mY, textXl } from '../../styles/atoms'; 3 | 4 | export const PanelTitle = styled.h1` 5 | ${textXl}; 6 | ${fontNormal}; 7 | ${mY(2)}; 8 | `; 9 | -------------------------------------------------------------------------------- /docs/api/Users/Remove Avatar.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Remove Avatar 3 | type: http 4 | seq: 11 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/remove_avatar 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | null 15 | } 16 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": ["src"], 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_members_by_channel.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | sm AS "space_member!: SpaceMember" 3 | FROM 4 | space_members sm 5 | INNER JOIN channels c ON c.space_id = sm.space_id 6 | WHERE 7 | sm.user_id = $1 8 | AND c.id = $2; 9 | 10 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/set_space_member.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | space_members 3 | SET 4 | is_admin = COALESCE($1, is_admin) 5 | WHERE 6 | user_id = $2 7 | AND space_id = $3 8 | RETURNING 9 | space_members AS "space_member!: SpaceMember"; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/sql/users/set_settings.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO users_extension (user_id, settings) 2 | VALUES ($1, $2) 3 | ON CONFLICT (user_id) 4 | DO UPDATE SET 5 | settings = $2 6 | RETURNING 7 | users_extension AS "user_ext!: UserExt"; 8 | 9 | -------------------------------------------------------------------------------- /docs/api/Users/Check Email Verification Status.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Check Email Verification Status 3 | type: http 4 | seq: 16 5 | } 6 | 7 | get { 8 | url: {{base_url}}/users/email_verification_status 9 | body: none 10 | auth: inherit 11 | } 12 | -------------------------------------------------------------------------------- /apps/server/migrations/20250930021446_remove-messages-foregin-keys.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE messages DROP CONSTRAINT IF EXISTS message_parent; 2 | ALTER TABLE messages DROP CONSTRAINT IF EXISTS message_channel; 3 | ALTER TABLE messages DROP CONSTRAINT IF EXISTS message_sender; 4 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_channel_member_list_by_user.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | m AS "member!: ChannelMember" 3 | FROM 4 | channel_members m 5 | INNER JOIN channels c ON c.id = m.channel_id 6 | WHERE 7 | m.user_id = $1 8 | AND m.is_joined; 9 | 10 | -------------------------------------------------------------------------------- /apps/server/sql/users/login.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | (PASSWORD = crypt($2, PASSWORD)) AS "password_match!", 3 | users AS "user!: User" 4 | FROM 5 | users 6 | WHERE (username = $1 7 | OR email = lower($1)) 8 | AND deactivated = FALSE 9 | LIMIT 1; 10 | 11 | -------------------------------------------------------------------------------- /apps/site/src/app/[lang]/[theme]/account/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingText } from '@boluo/ui/LoadingText'; 2 | 3 | export default function Loading() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /apps/spa/state/actions.ts: -------------------------------------------------------------------------------- 1 | export type MakeAction< 2 | ActionMap extends Record, 3 | ActionName, 4 | > = ActionName extends keyof ActionMap 5 | ? { 6 | type: ActionName; 7 | payload: ActionMap[ActionName]; 8 | } 9 | : never; 10 | -------------------------------------------------------------------------------- /apps/theme-designer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "theme-designer" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [dependencies] 7 | anyhow = "1.0" 8 | eframe = { version = "0.33", features = ["default"] } 9 | egui_extras = "0.33" 10 | once_cell = "1.19" 11 | -------------------------------------------------------------------------------- /docs/api/Spaces/Get Token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get Token 3 | type: http 4 | seq: 7 5 | } 6 | 7 | get { 8 | url: {{base_url}}/spaces/token?id={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api/Spaces/Members.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Members 3 | type: http 4 | seq: 6 5 | } 6 | 7 | get { 8 | url: {{base_url}}/spaces/members?id={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api/Users/Query User.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Query User 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{base_url}}/users/query?id={{user_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{user_id}} 15 | } 16 | -------------------------------------------------------------------------------- /packages/ui/src/HelpText.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { type ChildrenProps } from '@boluo/types'; 3 | 4 | export const HelpText: FC = ({ children }) => ( 5 |
{children}
6 | ); 7 | -------------------------------------------------------------------------------- /.taplo.toml: -------------------------------------------------------------------------------- 1 | [formatting] 2 | align_entries = false 3 | indent_tables = true 4 | indent_entries = true 5 | indent_string = ' ' 6 | 7 | [[rule]] 8 | include = ["**/Cargo.toml"] 9 | [rule.formatting] 10 | indent_tables = false 11 | indent_entries = false 12 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/TextArea.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { inputStyle } from './Input'; 3 | 4 | const TextArea = styled.textarea` 5 | ${inputStyle}; 6 | resize: none; 7 | width: 100%; 8 | `; 9 | 10 | export default TextArea; 11 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_channel_by_name.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | ch AS "channel!: Channel" 3 | FROM 4 | spaces s 5 | INNER JOIN channels ch ON ch.space_id = s.id 6 | WHERE 7 | s.id = $1 8 | AND ch.name = $2 9 | AND ch.deleted = FALSE 10 | LIMIT 1; 11 | 12 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_members_by_spaces.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | m AS "space!: SpaceMember", 3 | u AS "user!: User" 4 | FROM 5 | space_members m 6 | INNER JOIN users u ON u.id = m.user_id 7 | WHERE 8 | space_id = $1 9 | AND u.deactivated = FALSE; 10 | 11 | -------------------------------------------------------------------------------- /apps/server/text/reset-password/content.en.html: -------------------------------------------------------------------------------- 1 |

2 | You have requested to reset your password. 3 | Click here to reset your password. 4 |

5 | 6 |

If you did not request to reset your password, please ignore this email.

7 | -------------------------------------------------------------------------------- /apps/spa/components/sidebar/ConnectionIndicatorConnecting.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | export const ConnectionIndicatorConnecting: FC = () => { 5 | return ; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/spa/hooks/useIsDragging.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export const IsDraggingContext = createContext(false); 4 | 5 | export const useIsDragging = () => { 6 | const isDragging = useContext(IsDraggingContext); 7 | return isDragging; 8 | }; 9 | -------------------------------------------------------------------------------- /docs/api/Channels/Members.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Members 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: {{base_url}}/channels/members?id={{channel_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{channel_id}} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api/Spaces/Get Settings.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get Settings 3 | type: http 4 | seq: 8 5 | } 6 | 7 | get { 8 | url: {{base_url}}/spaces/settings?id={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/Title.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { m, p, pB, textXl } from '../../styles/atoms'; 3 | 4 | const Title = styled.h1` 5 | ${[textXl, p(0), pB(6), m(0)]}; 6 | font-weight: normal; 7 | `; 8 | 9 | export default Title; 10 | -------------------------------------------------------------------------------- /apps/server/src/ts.rs: -------------------------------------------------------------------------------- 1 | use specta_typescript::Typescript; 2 | 3 | pub fn export() { 4 | Typescript::default() 5 | .bigint(specta_typescript::BigIntExportBehavior::Number) 6 | .export_to("./packages/types/bindings.ts", &specta::export()) 7 | .unwrap(); 8 | } 9 | -------------------------------------------------------------------------------- /apps/server/text/email-verification/content.ja.html: -------------------------------------------------------------------------------- 1 |

こんにちは!

2 |

Boluoにご登録いただき、ありがとうございます。以下のリンクをクリックしてメールアドレスを認証してください:

3 |

メールアドレスを認証

4 |

このリンクは24時間後に期限切れになります。

5 |

もしBoluoアカウントにご登録されていない場合は、このメールを無視してください。

6 | -------------------------------------------------------------------------------- /apps/spa/components/PaneEmpty.tsx: -------------------------------------------------------------------------------- 1 | import { PaneSimpleBox } from './PaneSimpleBox'; 2 | 3 | export const PaneEmpty = () => { 4 | return ( 5 | 6 |
7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /apps/spa/hooks/useSpace.tsx: -------------------------------------------------------------------------------- 1 | import { type Space } from '@boluo/api'; 2 | import React from 'react'; 3 | 4 | /** The current space. */ 5 | export const SpaceContext = React.createContext(undefined); 6 | 7 | export const useSpace = () => React.useContext(SpaceContext); 8 | -------------------------------------------------------------------------------- /apps/spa/public/owlbear-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Boluo", 3 | "version": "1.0.0", 4 | "manifest_version": 1, 5 | "action": { 6 | "title": "Chat", 7 | "icon": "/icon.png", 8 | "popover": "/en", 9 | "height": 800, 10 | "width": 500 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/api/Users/Check Username.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Check Username 3 | type: http 4 | seq: 8 5 | } 6 | 7 | get { 8 | url: {{base_url}}/users/check_username?username=bruno 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | username: bruno 15 | } 16 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/Separator.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { lineColor } from '../../styles/colors'; 3 | 4 | const Separator = styled.hr` 5 | border: none; 6 | border-bottom: 1px solid ${lineColor}; 7 | `; 8 | 9 | export default Separator; 10 | -------------------------------------------------------------------------------- /apps/legacy/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/sql/channels/fetch_channel_with_space.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | ch AS "channel!: Channel", 3 | s AS "space!: Space" 4 | FROM 5 | channels ch 6 | INNER JOIN spaces s ON ch.space_id = s.id 7 | WHERE 8 | ch.id = $1 9 | AND ch.deleted = FALSE 10 | LIMIT 1; 11 | 12 | -------------------------------------------------------------------------------- /apps/server/sql/messages/export.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | msg AS "message!: Message" 3 | FROM 4 | messages msg 5 | WHERE 6 | msg.channel_id = $1 7 | AND msg.deleted = FALSE 8 | AND msg.created > coalesce($2, to_timestamp(0)::timestamptz) 9 | ORDER BY 10 | msg.pos; 11 | 12 | -------------------------------------------------------------------------------- /apps/server/sql/messages/get_after_pos.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | msg AS "message!: Message" 3 | FROM 4 | messages msg 5 | WHERE 6 | msg.channel_id = $1 7 | AND msg.deleted = FALSE 8 | AND ($2::float8 IS NULL OR msg.pos > $2) 9 | ORDER BY 10 | msg.pos ASC 11 | LIMIT $3; 12 | -------------------------------------------------------------------------------- /docs/api/Channels/All Members.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: All Members 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{base_url}}/channels/all_members?id={{channel_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{channel_id}} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api/Channels/Query Channel.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Query Channel 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/channels/query?id={{channel_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{channel_id}} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api/Messages/Query Message.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Query Message 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{base_url}}/messages/query?id={{message_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{message_id}} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api/Spaces/Query Space.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Query Space 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{base_url}}/spaces/query?id={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | token: 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/patch.ts: -------------------------------------------------------------------------------- 1 | import type { EditMessage, Message } from '@boluo/types/bindings'; 2 | 3 | export interface Patch { 4 | '/users/update_settings': { payload: object; query: null; result: object }; 5 | '/messages/edit': { payload: EditMessage; query: null; result: Message }; 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/src/OopsKaomoji.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | className?: string; 3 | } 4 | 5 | export const OopsKaomoji = ({ className }: Props) => { 6 | return ( 7 | 8 | (; ・`д・´) 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/fold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/legacy/src/hooks/useForceUpdate.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | 3 | export function useForceUpdate() { 4 | const [, setValue] = useState(0); // integer state 5 | return useCallback(() => setValue((value) => ++value), []); // update the state to force render 6 | } 7 | -------------------------------------------------------------------------------- /apps/server/sql/channels/remove_user_by_space.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | channel_members cm 3 | SET 4 | is_joined = FALSE 5 | FROM 6 | channels ch 7 | WHERE 8 | cm.user_id = $1 9 | AND ch.space_id = $2 10 | AND cm.channel_id = ch.id 11 | RETURNING 12 | cm.channel_id; 13 | 14 | -------------------------------------------------------------------------------- /apps/server/sql/channels/set_color.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | channel_members 3 | SET 4 | text_color = COALESCE($3, text_color) 5 | WHERE 6 | user_id = $1 7 | AND channel_id = $2 8 | AND is_joined 9 | RETURNING 10 | channel_members AS "channel_member!: ChannelMember"; 11 | 12 | -------------------------------------------------------------------------------- /apps/server/sql/channels/set_master.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | channel_members 3 | SET 4 | is_master = COALESCE($3, is_master) 5 | WHERE 6 | user_id = $1 7 | AND channel_id = $2 8 | AND is_joined 9 | RETURNING 10 | channel_members AS "channel_member!: ChannelMember"; 11 | 12 | -------------------------------------------------------------------------------- /apps/server/sql/users/partial_set_settings.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO users_extension (user_id, settings) 2 | VALUES ($1, $2) 3 | ON CONFLICT (user_id) 4 | DO UPDATE SET 5 | settings = users_extension.settings || $2 6 | RETURNING 7 | users_extension AS "user_ext!: UserExt"; 8 | 9 | -------------------------------------------------------------------------------- /apps/site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [{ "name": "next" }] 5 | }, 6 | "include": ["next.config.ts", "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 7 | "exclude": ["node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/spa/hooks/useComposeAtom.tsx: -------------------------------------------------------------------------------- 1 | import { type ChannelAtoms, useChannelAtoms } from './useChannelAtoms'; 2 | 3 | export type ComposeAtom = ChannelAtoms['composeAtom']; 4 | 5 | export const useComposeAtom = (): ChannelAtoms['composeAtom'] => { 6 | return useChannelAtoms().composeAtom; 7 | }; 8 | -------------------------------------------------------------------------------- /apps/spa/hooks/useMaxPane.tsx: -------------------------------------------------------------------------------- 1 | import { useBreakpoint } from '../breakpoint'; 2 | 3 | export const usePaneLimit = () => { 4 | const breakpoint = useBreakpoint(); 5 | if (breakpoint === 'xs' || breakpoint === 'sm') { 6 | return 1; 7 | } else { 8 | return 10; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /docs/api/Spaces/Get Users Status.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get Users Status 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/spaces/users_status?id={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api/Spaces/My Space Member.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: My Space Member 3 | type: http 4 | seq: 5 5 | } 6 | 7 | get { 8 | url: {{base_url}}/spaces/my_space_member?id={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | -------------------------------------------------------------------------------- /docs/api/Users/Check Email.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Check Email 3 | type: http 4 | seq: 9 5 | } 6 | 7 | get { 8 | url: {{base_url}}/users/check_email?email=bruno@example.com 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | email: bruno@example.com 15 | } 16 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/SpaceGrid.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { grid, spacingN } from '../../styles/atoms'; 3 | 4 | export const SpaceGrid = styled.div` 5 | ${grid}; 6 | grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); 7 | gap: ${spacingN(2)}; 8 | `; 9 | -------------------------------------------------------------------------------- /apps/legacy/src/hooks/useToggle.ts: -------------------------------------------------------------------------------- 1 | import { useReducer } from 'react'; 2 | 3 | const reducer = (prevState: boolean) => { 4 | return !prevState; 5 | }; 6 | 7 | export function useToggle(initialValue: boolean): [boolean, () => void] { 8 | return useReducer(reducer, initialValue); 9 | } 10 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_channel_member_list.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | m AS "member!: ChannelMember", 3 | u AS "user!: User" 4 | FROM 5 | channel_members m 6 | INNER JOIN users u ON u.id = m.user_id 7 | WHERE 8 | channel_id = $1 9 | AND ($2 10 | OR is_joined); 11 | 12 | -------------------------------------------------------------------------------- /apps/server/sql/channels/set_name.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | channel_members 3 | SET 4 | character_name = COALESCE($3, character_name) 5 | WHERE 6 | user_id = $1 7 | AND channel_id = $2 8 | AND is_joined 9 | RETURNING 10 | channel_members AS "channel_member!: ChannelMember"; 11 | 12 | -------------------------------------------------------------------------------- /packages/utils/src/async.ts: -------------------------------------------------------------------------------- 1 | export function sleep(ms: number): Promise { 2 | return new Promise((resolve) => setTimeout(resolve, ms)); 3 | } 4 | 5 | export function timeout(ms: number): Promise<'TIMEOUT'> { 6 | return new Promise((resolve) => setTimeout(() => resolve('TIMEOUT'), ms)); 7 | } 8 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_channel_member_list_by_user_and_space.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | m AS "member!: ChannelMember" 3 | FROM 4 | channel_members m 5 | INNER JOIN channels c ON c.id = m.channel_id 6 | WHERE 7 | m.user_id = $1 8 | AND c.space_id = $2 9 | AND m.is_joined; 10 | 11 | -------------------------------------------------------------------------------- /apps/spa/components/pane-channel/ChatContentLoading.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { Loading } from '@boluo/ui/Loading'; 3 | 4 | export const ChatListLoading: FC = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /apps/spa/state/ui.atoms.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { atomWithStorage } from 'jotai/utils'; 3 | 4 | export const isSidebarExpandedAtom = atomWithStorage('boluo-sidebar-expanded', true); 5 | 6 | export const sidebarContentStateAtom = atom<'CHANNELS' | 'SPACES' | 'CONNECTIONS'>('CHANNELS'); 7 | -------------------------------------------------------------------------------- /docs/api/Channels/Export.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Export 3 | type: http 4 | seq: 6 5 | } 6 | 7 | get { 8 | url: {{base_url}}/channels/export?channelId={{channel_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | channelId: {{channel_id}} 15 | after: 16 | } 17 | -------------------------------------------------------------------------------- /docs/api/Events/Get Token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get Token 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/events/token?spaceId={{space_id}}&userId 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | spaceId: {{space_id}} 15 | userId: 16 | } 17 | -------------------------------------------------------------------------------- /docs/api/Spaces/Query With Related.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Query With Related 3 | type: http 4 | seq: 4 5 | } 6 | 7 | get { 8 | url: {{base_url}}/spaces/query_with_related?id={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | -------------------------------------------------------------------------------- /packages/backend-proxy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@boluo/backend-proxy", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "@cloudflare/workers-types": "4", 6 | "typescript": "^5.9.3" 7 | }, 8 | "license": "AGPL-3.0", 9 | "scripts": { 10 | "build": "tsc" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/ui/src/entities/EntityUnknown.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | import { FormattedMessage } from 'react-intl'; 3 | 4 | export const EntityUnknown: FC = () => ( 5 | 6 | [] 7 | 8 | ); 9 | -------------------------------------------------------------------------------- /apps/legacy/src/hooks/useBaseUrlDelay.ts: -------------------------------------------------------------------------------- 1 | export { 2 | useBaseUrlDelay, 3 | getRouteScore, 4 | getRouteMovingAverage, 5 | getRouteSuccessRate, 6 | getAllRouteStats, 7 | resetRouteStats, 8 | } from './useBaseUrlMovingAverage'; 9 | export type { MeasureResult } from './useBaseUrlMovingAverage'; 10 | -------------------------------------------------------------------------------- /apps/legacy/src/utils/profile.ts: -------------------------------------------------------------------------------- 1 | import { ONLINE_TIMEOUT } from '../settings'; 2 | 3 | export const isOnline = (timestamp?: number, now?: number) => { 4 | if (!timestamp) { 5 | return false; 6 | } 7 | now = now || new Date().getTime(); 8 | return now - timestamp < ONLINE_TIMEOUT; 9 | }; 10 | -------------------------------------------------------------------------------- /apps/server/text/email-change/content.ja.html: -------------------------------------------------------------------------------- 1 |

こんにちは!

2 |

Boluoアカウントのメールアドレス変更をリクエストされました。以下のリンクをクリックして変更を確認してください:

3 |

メールアドレス変更を確認

4 |

このリンクは24時間後に有効期限が切れます。

5 |

メールアドレス変更をリクエストしていない場合は、このメールを無視してください。現在のメールアドレスは変更されません。

6 | -------------------------------------------------------------------------------- /apps/spa/hooks/useBannerNode.tsx: -------------------------------------------------------------------------------- 1 | import React, { type RefObject } from 'react'; 2 | 3 | export const BannerContext = React.createContext>({ 4 | current: null, 5 | }); 6 | 7 | export const useBannerNode = (): HTMLDivElement | null => React.useContext(BannerContext).current; 8 | -------------------------------------------------------------------------------- /docs/api/Channels/Check Name.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Check Name 3 | type: http 4 | seq: 5 5 | } 6 | 7 | get { 8 | url: {{base_url}}/channels/check_name?name&spaceId={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | name: 15 | spaceId: {{space_id}} 16 | } 17 | -------------------------------------------------------------------------------- /packages/api/src/origin-map.ts: -------------------------------------------------------------------------------- 1 | export const originMap = { 2 | 'boluochat.com': 'https://production.boluochat.com', 3 | 'boluo.chat': 'https://production.boluo.chat', 4 | 'boluo-staging.mythal.net': 'https://server.boluo-staging.mythal.net', 5 | '.kagangtuya.top': 'https://boluo-net.kagangtuya.top', 6 | }; 7 | -------------------------------------------------------------------------------- /packages/icons/icons/play.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "moduleResolution": "bundler", 5 | "module": "esnext", 6 | "rootDir": "src" 7 | }, 8 | "include": ["src"], 9 | "exclude": ["dist", "build", "node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/spa/hooks/useMember.tsx: -------------------------------------------------------------------------------- 1 | import { type MemberWithUser } from '@boluo/api'; 2 | import React from 'react'; 3 | 4 | export const MemberContext = React.createContext(null); 5 | 6 | export const useMember = (): MemberWithUser | null => { 7 | return React.useContext(MemberContext); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/icons/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "outDir": "./dist/" 6 | }, 7 | "include": ["src/**/*.ts", "src/**/*.tsx", "build.ts"], 8 | "exclude": ["dist", "build", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/settings/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "module": "Preserve", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["dist", "build", "node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/types/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "module": "Preserve", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["**/*.ts"], 9 | "exclude": ["dist", "build", "node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/legacy/src/information.ts: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from 'react'; 2 | import { type Id } from './utils/id'; 3 | 4 | export type InformationLevel = 'INFO' | 'SUCCESS' | 'WARNING' | 'ERROR'; 5 | 6 | export interface Information { 7 | id: Id; 8 | level: InformationLevel; 9 | content: ReactNode; 10 | } 11 | -------------------------------------------------------------------------------- /apps/server/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | dotenvy::from_filename(".env.local").ok(); 3 | dotenvy::dotenv().ok(); 4 | 5 | if let Ok(url) = std::env::var("DATABASE_URL") { 6 | println!("cargo::rustc-env=DATABASE_URL={url}"); 7 | println!("cargo:rerun-if-changed=migrations"); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /apps/server/sql/messages/create.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO messages (id, sender_id, channel_id, name, text, entities, in_game, is_action, is_master, whisper_to_users, media_id, pos_p, pos_q, color) 2 | VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) 3 | RETURNING 4 | messages AS "message!: Message"; 5 | 6 | -------------------------------------------------------------------------------- /apps/server/sql/messages/get_by_channel.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | msg AS "message!: Message" 3 | FROM 4 | messages msg 5 | WHERE 6 | msg.channel_id = $1 7 | AND msg.deleted = FALSE 8 | AND ($2::float8 IS NULL 9 | OR msg.pos < $2) -- before 10 | ORDER BY 11 | msg.pos DESC 12 | LIMIT $3; 13 | 14 | -------------------------------------------------------------------------------- /apps/server/sql/users/edit.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | users 3 | SET 4 | nickname = COALESCE($2, nickname), 5 | bio = COALESCE($3, bio), 6 | avatar_id = COALESCE($4, avatar_id), 7 | default_color = COALESCE($5, default_color) 8 | WHERE 9 | id = $1 10 | RETURNING 11 | users AS "users!: User"; 12 | 13 | -------------------------------------------------------------------------------- /apps/site/src/server.ts: -------------------------------------------------------------------------------- 1 | import { type IntlShape } from 'react-intl'; 2 | 3 | export interface Params { 4 | lang: string; 5 | theme: string; 6 | } 7 | 8 | export const title = (intl: IntlShape, prefix: string): string => { 9 | return prefix + ' - ' + intl.formatMessage({ defaultMessage: 'Boluo' }); 10 | }; 11 | -------------------------------------------------------------------------------- /docs/api/Spaces/Delete Space.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Delete Space 3 | type: http 4 | seq: 11 5 | } 6 | 7 | post { 8 | url: {{base_url}}/spaces/delete?id={{space_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | 17 | body:json { 18 | {} 19 | } 20 | -------------------------------------------------------------------------------- /docs/api/Spaces/Leave Space.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Leave Space 3 | type: http 4 | seq: 14 5 | } 6 | 7 | post { 8 | url: {{base_url}}/spaces/leave?id={{space_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | 17 | body:json { 18 | {} 19 | } 20 | -------------------------------------------------------------------------------- /docs/api/Users/Query Self.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Query Self 3 | type: http 4 | seq: 5 5 | } 6 | 7 | get { 8 | url: {{base_url}}/users/query_self 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | script:post-response { 14 | let data = res.getBody(); 15 | bru.setVar("user_id", data.ok.id); 16 | } 17 | -------------------------------------------------------------------------------- /apps/legacy/src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | export function compare(a: number, b: number): number { 2 | return b - a; 3 | } 4 | 5 | export function compareRev(a: number, b: number): number { 6 | return a - b; 7 | } 8 | 9 | export function parseDateString(dateString: string): Date { 10 | return new Date(dateString); 11 | } 12 | -------------------------------------------------------------------------------- /apps/legacy/src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import { encodeUuid, type Id } from './id'; 2 | 3 | export function chatPath(spaceId: Id, channelId?: Id): string { 4 | if (channelId) { 5 | return `/chat/${encodeUuid(spaceId)}/${encodeUuid(channelId)}`; 6 | } else { 7 | return `/chat/${encodeUuid(spaceId)}`; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/api/Users/Get Me.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Get Me 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/users/get_me 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | script:post-response { 14 | let data = res.getBody(); 15 | let token = bru.setVar("user_id", data.ok.user.id); 16 | } 17 | -------------------------------------------------------------------------------- /docs/api/Users/Resend Email Verification.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Resend Email Verification 3 | type: http 4 | seq: 17 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/resend_email_verification 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "lang": "zh-CN" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/color/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "module": "Preserve", 5 | "moduleResolution": "bundler", 6 | "rootDir": "src", 7 | "outDir": "dist" 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules", "dist"] 11 | } 12 | -------------------------------------------------------------------------------- /docs/api/Channels/Join Channel.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Join Channel 3 | type: http 4 | seq: 8 5 | } 6 | 7 | post { 8 | url: {{base_url}}/channels/join 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "channelId": "{{channel_id}}", 16 | "characterName": "Link" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/api/Channels/Leave Channel.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Leave Channel 3 | type: http 4 | seq: 9 5 | } 6 | 7 | post { 8 | url: {{base_url}}/channels/leave?id={{channel_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{channel_id}} 15 | } 16 | 17 | body:json { 18 | {} 19 | } 20 | -------------------------------------------------------------------------------- /docs/api/Messages/Delete Message.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Delete Message 3 | type: http 4 | seq: 5 5 | } 6 | 7 | post { 8 | url: {{base_url}}/messages/delete?id={{message_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{message_id}} 15 | } 16 | 17 | body:json { 18 | {} 19 | } 20 | -------------------------------------------------------------------------------- /docs/api/Messages/Toggle Fold.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Toggle Fold 3 | type: http 4 | seq: 6 5 | } 6 | 7 | post { 8 | url: {{base_url}}/messages/toggle_fold?id={{message_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{message_id}} 15 | } 16 | 17 | body:json { 18 | {} 19 | } 20 | -------------------------------------------------------------------------------- /docs/api/Spaces/Refresh Token.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Refresh Token 3 | type: http 4 | seq: 12 5 | } 6 | 7 | post { 8 | url: {{base_url}}/spaces/refresh_token?id={{space_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | 17 | body:json { 18 | {} 19 | } 20 | -------------------------------------------------------------------------------- /docs/api/Users/Confirm Email Change.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Confirm Email Change 3 | type: http 4 | seq: 15 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/confirm_email_change 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "token": "email_change_token_here" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/api/Users/Reset Password.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Reset Password 3 | type: http 4 | seq: 12 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/reset_password 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "email": "user@example.com", 16 | "lang": "zh-CN" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/icons/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/api/Channels/Delete Channel.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Delete Channel 3 | type: http 4 | seq: 12 5 | } 6 | 7 | post { 8 | url: {{base_url}}/channels/delete?id={{channel_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{channel_id}} 15 | } 16 | 17 | body:json { 18 | {} 19 | } 20 | -------------------------------------------------------------------------------- /packages/common/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "moduleResolution": "bundler", 6 | "module": "Preserve" 7 | }, 8 | "include": ["**/*.ts", "**/*.tsx"], 9 | "exclude": ["dist", "build", "node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/hooks/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "moduleResolution": "bundler", 6 | "module": "Preserve" 7 | }, 8 | "include": ["**/*.ts", "**/*.tsx"], 9 | "exclude": ["dist", "build", "node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /packages/hooks/useQueryMySpaces.tsx: -------------------------------------------------------------------------------- 1 | import { type ApiError, type SpaceWithMember } from '@boluo/api'; 2 | import { type SWRResponse } from 'swr'; 3 | import { useGetQuery } from './useGetQuery'; 4 | 5 | export const useQueryMySpaces = (): SWRResponse => { 6 | return useGetQuery('/spaces/my', null); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/icons/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/theme/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "module": "Preserve", 6 | "moduleResolution": "bundler" 7 | }, 8 | "include": ["**/*.ts", "**/*.tsx"], 9 | "exclude": ["dist", "build", "node_modules"] 10 | } 11 | -------------------------------------------------------------------------------- /apps/legacy/src/components/chat/ListItemPlaceholder.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import * as React from 'react'; 3 | 4 | const style = css` 5 | width: 100%; 6 | height: 100%; 7 | `; 8 | 9 | function ListItemPlaceholder() { 10 | return
; 11 | } 12 | 13 | export default ListItemPlaceholder; 14 | -------------------------------------------------------------------------------- /docs/api/Spaces/Join Space.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Join Space 3 | type: http 4 | seq: 13 5 | } 6 | 7 | post { 8 | url: {{base_url}}/spaces/join?spaceId={{space_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | spaceId: {{space_id}} 15 | token: 16 | } 17 | 18 | body:json { 19 | {} 20 | } 21 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist/", 6 | "module": "Preserve", 7 | "moduleResolution": "bundler" 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["dist", "build", "node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/icons/icons/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/sort/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "module": "Preserve", 7 | "moduleResolution": "bundler" 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["dist", "node_modules"] 11 | } 12 | 13 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/components/molecules/SpinnerIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Fan from '../../fan.svg'; 3 | import { spin } from '../../styles/atoms'; 4 | import Icon from '../atoms/Icon'; 5 | 6 | function SpinnerIcon() { 7 | return ; 8 | } 9 | 10 | export default React.memo(SpinnerIcon); 11 | -------------------------------------------------------------------------------- /apps/server/sql/channels/edit_member.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | channel_members 3 | SET 4 | character_name = COALESCE($3, character_name), 5 | text_color = COALESCE($4, text_color) 6 | WHERE 7 | user_id = $1 8 | AND channel_id = $2 9 | AND is_joined 10 | RETURNING 11 | channel_members AS "member!: ChannelMember"; 12 | 13 | -------------------------------------------------------------------------------- /apps/spa/components/pane-channel-settings/form.ts: -------------------------------------------------------------------------------- 1 | import { type ChannelType } from '@boluo/api'; 2 | 3 | export interface ChannelSettingsForm { 4 | name: string; 5 | topic: string; 6 | defaultDiceType: string; 7 | defaultRollCommand: string; 8 | isSecret: boolean; 9 | type: ChannelType; 10 | isArchived: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /packages/icons/icons/chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/paper-plane.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/interpreter/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "module": "Preserve", 7 | "moduleResolution": "bundler" 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["dist", "node_modules"] 11 | } 12 | 13 | -------------------------------------------------------------------------------- /packages/locale/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist/", 6 | "module": "Preserve", 7 | "moduleResolution": "bundler" 8 | }, 9 | "include": ["src/**/*.ts"], 10 | "exclude": ["dist", "build", "node_modules"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/utils/src/function.ts: -------------------------------------------------------------------------------- 1 | export const not = (x: unknown): boolean => !x; 2 | export const toggle = not; 3 | 4 | // identity 5 | export type SelfMapper = (x: T) => T; 6 | export const identity = (x: T): T => x; 7 | 8 | export type EmptyFunction = () => void; 9 | export const empty = () => { 10 | // empty function 11 | }; 12 | -------------------------------------------------------------------------------- /apps/server/sql/users/get.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | users AS "users!: User" 3 | FROM 4 | users 5 | WHERE 6 | CASE WHEN $1 IS NOT NULL THEN 7 | id = $1 8 | WHEN $2 IS NOT NULL THEN 9 | email = $2 10 | WHEN $3 IS NOT NULL THEN 11 | username = $3 12 | END 13 | AND deactivated = FALSE 14 | LIMIT 1; 15 | 16 | -------------------------------------------------------------------------------- /apps/spa/components/sidebar/SidebarContentLoading.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | import { LoadingText } from '@boluo/ui/LoadingText'; 3 | 4 | export const SidebarContentLoading: FC = () => { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /docs/api/Spaces/Kick Member.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Kick Member 3 | type: http 4 | seq: 15 5 | } 6 | 7 | post { 8 | url: {{base_url}}/spaces/kick?spaceId={{space_id}}&userId 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | spaceId: {{space_id}} 15 | userId: 16 | } 17 | 18 | body:json { 19 | {} 20 | } 21 | -------------------------------------------------------------------------------- /docs/api/Users/Request Email Change.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Request Email Change 3 | type: http 4 | seq: 14 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/request_email_change 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "newEmail": "new@example.com", 16 | "lang": "zh-CN" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/icons/icons/chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/cloud.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/utils/src/files.ts: -------------------------------------------------------------------------------- 1 | export const showFileSize = (size: number): string => { 2 | if (size < 1024) return `${size} Byte`; 3 | if (size < 1024 * 1024) return `${Math.round(size / 1024)} KiB`; 4 | if (size < 1024 * 1024 * 1024) return `${Math.round(size / 1024 / 1024)} MiB`; 5 | return `${Math.round(size / 1024 / 1024 / 1024)} GB`; 6 | }; 7 | -------------------------------------------------------------------------------- /apps/server/sql/messages/move_between.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | messages 3 | SET 4 | (pos_p, 5 | pos_q) = ( 6 | SELECT 7 | p AS pos_p, 8 | q AS pos_q 9 | FROM 10 | find_intermediate ($2, $3, $4, $5)) 11 | WHERE 12 | id = $1 13 | RETURNING 14 | messages AS "message!: Message"; 15 | 16 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get_space_member_list_by_user.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | sm AS "member!: SpaceMember" 3 | FROM 4 | space_members sm 5 | INNER JOIN spaces s ON sm.space_id = s.id 6 | AND s.deleted = FALSE 7 | INNER JOIN users u ON u.id = $1 8 | WHERE 9 | sm.user_id = $1 10 | ORDER BY 11 | s.latest_activity DESC; 12 | 13 | -------------------------------------------------------------------------------- /packages/icons/icons/moon.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /packages/icons/icons/pause.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/columns.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/News.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { p, roundedSm, textSm } from '../../styles/atoms'; 3 | import { blue } from '../../styles/colors'; 4 | 5 | export const News = styled.div` 6 | ${[roundedSm, p(2), textSm]}; 7 | background-color: ${blue['900']}; 8 | border: 1px solid ${blue['800']}; 9 | `; 10 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/Portal.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactNode } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | const root = document.getElementById('portal') as HTMLDivElement; 5 | 6 | export const Portal: React.FC<{ children: ReactNode }> = React.memo(({ children }) => { 7 | return ReactDOM.createPortal(children, root); 8 | }); 9 | -------------------------------------------------------------------------------- /apps/server/sql/messages/edit.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | messages 3 | SET 4 | name = $2, 5 | text = $3, 6 | entities = $4, 7 | in_game = $5, 8 | is_action = $6, 9 | media_id = $7, 10 | modified = (now() at time zone 'utc'), 11 | color = $8 12 | WHERE 13 | id = $1 14 | RETURNING 15 | messages AS "message!: Message"; 16 | 17 | -------------------------------------------------------------------------------- /docs/api/Channels/Add Member.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Add Member 3 | type: http 4 | seq: 14 5 | } 6 | 7 | post { 8 | url: {{base_url}}/channels/add_member 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "channelId": "{{channel_id}}", 16 | "userId": "", 17 | "characterName": "路过的魔法少女" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/api/Channels/Edit Master.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Edit Master 3 | type: http 4 | seq: 15 5 | } 6 | 7 | post { 8 | url: {{base_url}}/channels/edit_master 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "channelId": "{{channel_id}}", 16 | "userId": "", 17 | "grantOrRevoke": "GRANT" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/api/src/events.ts: -------------------------------------------------------------------------------- 1 | export type * from '@boluo/types/bindings'; 2 | import type { Update } from '@boluo/types/bindings'; 3 | 4 | export function isServerUpdate(object: unknown): object is Update { 5 | if (typeof object !== 'object' || object == null) { 6 | return false; 7 | } 8 | return 'mailbox' in object && 'body' in object; 9 | } 10 | -------------------------------------------------------------------------------- /packages/icons/icons/bold.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/interpreter-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "rootDir": "src", 5 | "outDir": "dist", 6 | "types": ["node"], 7 | "module": "ESNext", 8 | "moduleResolution": "Bundler" 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": ["dist", "node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /apps/site/.vercel/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "_", 3 | "orgId": "_", 4 | "settings": { 5 | "framework": "nextjs", 6 | "rootDirectory": null, 7 | "installCommand": "npm install --include-workspace-root", 8 | "outputDirectory": null, 9 | "buildCommand": "npm run --include-workspace-root build:site --if-present" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /apps/spa/state/notification.atoms.ts: -------------------------------------------------------------------------------- 1 | import { atomWithStorage } from 'jotai/utils'; 2 | 3 | export const isNotificationSupported = typeof window !== 'undefined' && 'Notification' in window; 4 | export const notificationEnableAtom = atomWithStorage( 5 | 'boluo-notification-v0', 6 | isNotificationSupported && Notification.permission === 'granted', 7 | ); 8 | -------------------------------------------------------------------------------- /docs/api/Users/Reset Password Confirm.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Reset Password Confirm 3 | type: http 4 | seq: 13 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/reset_password_confirm 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "token": "reset_token_here", 16 | "password": "new_password" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { lighten } from 'polished'; 3 | import { textSm } from '../../styles/atoms'; 4 | import { errorColor } from '../../styles/colors'; 5 | 6 | export const ErrorMessage = styled.p` 7 | margin: 0; 8 | ${textSm}; 9 | color: ${lighten(0.5, errorColor)}; 10 | `; 11 | -------------------------------------------------------------------------------- /apps/legacy/src/utils/errors.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { showFlash } from '../actions'; 3 | import { type AppError, errorText } from '../api/error'; 4 | import { type Dispatch } from '../store'; 5 | 6 | export const throwErr = (dispatch: Dispatch) => (err: AppError) => { 7 | dispatch(showFlash('ERROR', {errorText(err).description})); 8 | }; 9 | -------------------------------------------------------------------------------- /apps/server/src/events.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | mod broadcast; 3 | pub mod context; 4 | mod handlers; 5 | pub mod models; 6 | pub mod preview; 7 | mod status; 8 | mod token; 9 | mod types; 10 | 11 | pub use broadcast::{get_broadcast_table, get_mailbox_broadcast_rx}; 12 | pub use handlers::router; 13 | pub use status::StatusMap; 14 | pub use types::{Update, startup_id}; 15 | -------------------------------------------------------------------------------- /apps/server/text/email-verification/content.en.html: -------------------------------------------------------------------------------- 1 |

Hello!

2 |

Thank you for registering with Boluo. Please click the link below to verify your email address:

3 |

Verify Email

4 |

This link will expire in 24 hours.

5 |

If you didn't register for a Boluo account, please ignore this email.

6 | -------------------------------------------------------------------------------- /apps/spa/components/PaneFooterBox.tsx: -------------------------------------------------------------------------------- 1 | import type { FC } from 'react'; 2 | import { type ChildrenProps } from '@boluo/types'; 3 | 4 | export const PaneFooterBox: FC = ({ children }) => ( 5 |
6 | {children} 7 |
8 | ); 9 | -------------------------------------------------------------------------------- /apps/spa/hooks/useComposeError.tsx: -------------------------------------------------------------------------------- 1 | import { useAtomValue } from 'jotai'; 2 | import { type ComposeError } from '../state/compose.reducer'; 3 | import { useChannelAtoms } from './useChannelAtoms'; 4 | 5 | export const useComposeError = (): ComposeError | null => { 6 | const { checkComposeAtom } = useChannelAtoms(); 7 | return useAtomValue(checkComposeAtom); 8 | }; 9 | -------------------------------------------------------------------------------- /apps/spa/sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | import { SENTRY_CONFIG } from './const'; 3 | 4 | if (SENTRY_CONFIG.enabled) { 5 | Sentry.init({ 6 | dsn: SENTRY_CONFIG.dsn, 7 | environment: 'development', 8 | tunnel: SENTRY_CONFIG.tunnel, 9 | attachStacktrace: true, 10 | integrations: [], 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /docs/api/Channels/Edit Member.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Edit Member 3 | type: http 4 | seq: 13 5 | } 6 | 7 | post { 8 | url: {{base_url}}/channels/edit_member 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "channelId": "{{channel_id}}", 16 | "characterName": "阿拉拉圾", 17 | "textColor": "#ff0000" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/icons/icons/eye.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/types/utils.ts: -------------------------------------------------------------------------------- 1 | import type React from 'react'; 2 | 3 | export type Empty = Record; 4 | 5 | export interface StyleProps { 6 | className?: string | undefined; 7 | } 8 | 9 | export type DataAttr = { [P in keyof T & string as `data-${P}`]?: T[P] }; 10 | 11 | export interface ChildrenProps { 12 | children: React.ReactNode; 13 | } 14 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/rotate-cw.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/spa/hooks/useScrollerRef.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { createContext, type RefObject } from 'react'; 3 | 4 | export const ScrollerRefContext = createContext>({ 5 | current: null, 6 | }); 7 | 8 | export const useScrollerRef = (): RefObject => 9 | useContext(ScrollerRefContext); 10 | -------------------------------------------------------------------------------- /docs/api/Messages/Move Between.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Move Between 3 | type: http 4 | seq: 4 5 | } 6 | 7 | post { 8 | url: {{base_url}}/messages/move_between 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "messageId": "{{message_id}}", 16 | "range": [null, null], 17 | "channelId": "{{channel_id}}" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /docs/api/Spaces/Update Settings.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Update Settings 3 | type: http 4 | seq: 16 5 | } 6 | 7 | post { 8 | url: {{base_url}}/spaces/update_settings?id={{space_id}} 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | 17 | body:json { 18 | { 19 | "nanigasuki": "チョコミント" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /apps/spa/components/sidebar/SidebarSkeletonItem.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingText } from '@boluo/ui/LoadingText'; 2 | import { SidebarItem } from './SidebarItem'; 3 | 4 | const nop = () => { 5 | // no-op 6 | }; 7 | export const SidebarSkeletonItem = () => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /apps/spa/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /docs/api/Spaces/My Spaces.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: My Spaces 3 | type: http 4 | seq: 3 5 | } 6 | 7 | get { 8 | url: {{base_url}}/spaces/my 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | script:post-response { 14 | let data = res.getBody(); 15 | if (data.isOk && data.ok.length > 0) { 16 | bru.setVar("space_id", data.ok[0].space.id); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/api/Users/Register.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Register 3 | type: http 4 | seq: 3 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/register 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "email": "bruno@example.com", 16 | "username": "bruno", 17 | "nickname": "Bruno", 18 | "password": "BrunoBruno" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/api-browser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "lib": ["ESNext", "DOM"], 6 | "outDir": "dist/", 7 | "module": "Preserve", 8 | "moduleResolution": "bundler" 9 | }, 10 | "include": ["src/**/*.ts"], 11 | "exclude": ["dist", "build", "node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/hooks/useMounted.tsx: -------------------------------------------------------------------------------- 1 | import { type RefObject, useEffect, useRef } from 'react'; 2 | 3 | export const useMountedRef = (): RefObject => { 4 | const mountedRef = useRef(true); 5 | useEffect(() => { 6 | mountedRef.current = true; 7 | return () => { 8 | mountedRef.current = false; 9 | }; 10 | }, []); 11 | return mountedRef; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/icons/icons/plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/spa/hooks/useSettings.tsx: -------------------------------------------------------------------------------- 1 | import type { Settings } from '@boluo/settings'; 2 | import React from 'react'; 3 | 4 | const emptySettings: Settings = {}; 5 | 6 | export const SettingsContext = React.createContext(emptySettings); 7 | 8 | export const useSettings = () => { 9 | return React.useContext(SettingsContext) || emptySettings; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/icons/icons/moon-star.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/uncheck.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/legacy/src/components/chat/PrivateChat.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import React from 'react'; 3 | 4 | const style = css` 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | justify-content: center; 9 | align-items: center; 10 | `; 11 | 12 | export const PrivateChat = () => { 13 | return
这是私有频道,你没有加入或者被邀请。
; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/api/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface LoginData { 2 | username: string; 3 | password: string; 4 | withToken?: boolean; 5 | } 6 | 7 | export interface SpaceIdWithToken { 8 | spaceId: string; 9 | token?: string; 10 | } 11 | export interface IdQuery { 12 | id: string; 13 | } 14 | 15 | export interface IdWithToken { 16 | id: string; 17 | token?: string; 18 | } 19 | -------------------------------------------------------------------------------- /packages/icons/icons/archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/icons/icons/bell.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/filter-x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/user.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/spa/hooks/useChannelId.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from 'react'; 2 | 3 | export const ChannelIdContext = createContext(null); 4 | 5 | export const useChannelId = (): string => { 6 | const id = useContext(ChannelIdContext); 7 | if (!id) { 8 | throw new Error('[Unexpected] Access channel id outside the channel'); 9 | } 10 | return id; 11 | }; 12 | -------------------------------------------------------------------------------- /apps/spa/production/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "boluo-app" 2 | compatibility_date = "2025-10-19" 3 | main = "../../../packages/backend-proxy/dist/worker.js" 4 | 5 | [assets] 6 | directory = "../out/" 7 | html_handling = "auto-trailing-slash" 8 | not_found_handling = "404-page" 9 | run_worker_first = ["/api/*"] 10 | 11 | [vars] 12 | BACKEND_URL = "https://production.boluo.chat" 13 | -------------------------------------------------------------------------------- /apps/spa/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@boluo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "plugins": [{ "name": "next" }] 5 | }, 6 | "include": [ 7 | "next-env.d.ts", 8 | "additional.d.ts", 9 | "next.config.ts", 10 | "**/*.ts", 11 | "**/*.tsx", 12 | ".next/types/**/*.ts" 13 | ], 14 | "exclude": ["node_modules", "out"] 15 | } 16 | -------------------------------------------------------------------------------- /apps/spa/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "boluo-app-staging" 2 | compatibility_date = "2025-10-19" 3 | main = "../../packages/backend-proxy/dist/worker.js" 4 | 5 | [assets] 6 | directory = "./out/" 7 | html_handling = "auto-trailing-slash" 8 | not_found_handling = "404-page" 9 | run_worker_first = ["/api/*"] 10 | 11 | [vars] 12 | BACKEND_URL = "https://boluo-server-staging.fly.dev" 13 | -------------------------------------------------------------------------------- /docs/api/Media/Presigned.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Presigned 3 | type: http 4 | seq: 1 5 | } 6 | 7 | post { 8 | url: {{base_url}}/media/presigned?filename=malice.png&mimeType=image/png&size=1024 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | filename: malice.png 15 | mimeType: image/png 16 | size: 1024 17 | } 18 | 19 | body:json { 20 | {} 21 | } 22 | -------------------------------------------------------------------------------- /packages/icons/icons/chevrons-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/x-circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /apps/legacy/src/components/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useTitle } from '../../hooks/useTitle'; 3 | import Title from '../atoms/Title'; 4 | 5 | function NotFound() { 6 | useTitle('页面没有找到'); 7 | return ( 8 | <> 9 | 没有找到 10 |

所请求的资源不存在、已删除或者你没有权限访问。

11 | 12 | ); 13 | } 14 | 15 | export default NotFound; 16 | -------------------------------------------------------------------------------- /apps/storybook/src/Spinner.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | 3 | import { Spinner } from '@boluo/ui/Spinner'; 4 | 5 | const meta: Meta = { 6 | title: 'Feedback/Spinner', 7 | component: Spinner, 8 | }; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Basic: Story = { args: {} }; 14 | -------------------------------------------------------------------------------- /packages/icons/icons/history.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "plugins": [{ "name": "next" }], 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "allowJs": true, 9 | "jsx": "preserve", 10 | "noEmit": true, 11 | "resolveJsonModule": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/src/BackToHomepage.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from 'react-intl'; 2 | import * as classes from './classes'; 3 | 4 | export const BackToHomepage = ({ url }: { url: string }) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/ui/src/entities/EntityText.tsx: -------------------------------------------------------------------------------- 1 | import { type EntityOf } from '@boluo/api'; 2 | import type { FC } from 'react'; 3 | 4 | interface Props { 5 | source: string; 6 | entity: EntityOf<'Text'>; 7 | } 8 | 9 | export const EntityText: FC = ({ source, entity: { start, len } }) => { 10 | return {source.substring(start, start + len)}; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ui/src/users/UserCardError.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from 'react-intl'; 2 | import { FloatingBox } from '../FloatingBox'; 3 | 4 | export const UserCardError = () => { 5 | return ( 6 | 7 |
8 | 9 |
10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/ui/src/users/UserCardLoading.tsx: -------------------------------------------------------------------------------- 1 | import { FormattedMessage } from 'react-intl'; 2 | import { FloatingBox } from '../FloatingBox'; 3 | 4 | export const UserCardLoading = () => { 5 | return ( 6 | 7 |
8 | 9 |
10 |
11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/lock.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/HelpText.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { block, spacingN, textSm } from '../../styles/atoms'; 3 | import { minorTextColor } from '../../styles/colors'; 4 | 5 | export const HelpText = styled.small` 6 | ${[textSm, block]}; 7 | margin: 0; 8 | padding: ${spacingN(1)} 0; 9 | line-height: 1.5em; 10 | color: ${minorTextColor}; 11 | `; 12 | -------------------------------------------------------------------------------- /packages/icons/icons/sidebar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/dot-circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /apps/legacy/src/states/connection.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { atomWithStorage } from 'jotai/utils'; 3 | import { isCrossOrigin } from '../settings'; 4 | 5 | export type ConnectState = 'CONNECTING' | 'OPEN' | 'CLOSED'; 6 | export const connectionStateAtom = atom('CLOSED'); 7 | 8 | export const autoSelectAtom = atomWithStorage('BOLUO_AUTO_SELECT_PROXY', !isCrossOrigin); 9 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/get.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | s AS "space!: Space", 3 | u AS "user!: User" 4 | FROM 5 | spaces s 6 | LEFT JOIN users u ON CASE WHEN $3 THEN 7 | s.owner_id = u.id 8 | END 9 | WHERE 10 | CASE WHEN $1 IS NOT NULL THEN 11 | s.id = $1 12 | WHEN $2 IS NOT NULL THEN 13 | s.name = $2 14 | END 15 | AND deleted = FALSE 16 | LIMIT 1; 17 | 18 | -------------------------------------------------------------------------------- /apps/spa/hooks/useReadObserve.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { recordWarn } from '../error'; 3 | 4 | export const ReadObserverContext = React.createContext<(node: Element) => () => void>(() => { 5 | recordWarn('ReadObserverContext is not provided'); 6 | return () => {}; 7 | }); 8 | 9 | export const useReadObserve = () => { 10 | return React.useContext(ReadObserverContext); 11 | }; 12 | -------------------------------------------------------------------------------- /apps/storybook/src/LoadFailed.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { LoadFailed } from '@boluo/ui/LoadFailed'; 3 | 4 | const meta: Meta = { title: 'Feedback/LoadFailed', component: LoadFailed }; 5 | 6 | export default meta; 7 | type Story = StoryObj; 8 | 9 | export const Default: Story = { 10 | args: {}, 11 | }; 12 | -------------------------------------------------------------------------------- /docker-compose.override.example.yml: -------------------------------------------------------------------------------- 1 | # Example override file for development environment 2 | # Copy this file to docker-compose.override.yml and edit it to your needs 3 | 4 | services: 5 | db: 6 | ports: 7 | - 127.0.0.1:5432:5432 8 | redis: 9 | ports: 10 | - 127.0.0.1:6379:6379 11 | storage: 12 | ports: 13 | - 127.0.0.1:9000:9000 14 | - 127.0.0.1:9090:9090 15 | -------------------------------------------------------------------------------- /docs/api/Channels/Kick Member.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Kick Member 3 | type: http 4 | seq: 10 5 | } 6 | 7 | post { 8 | url: {{base_url}}/channels/kick?spaceId={{space_id}}&channelId={{channel_id}}&userId 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | spaceId: {{space_id}} 15 | channelId: {{channel_id}} 16 | userId: 17 | } 18 | 19 | body:json { 20 | {} 21 | } 22 | -------------------------------------------------------------------------------- /packages/icons/icons/panel-left-open.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/interpreter/src/to-parsed.ts: -------------------------------------------------------------------------------- 1 | import { type ParseResult, emptyParseResult } from './parse-result'; 2 | import { type Entities } from '@boluo/api'; 3 | 4 | export const messageToParsed = (text: string, entities: Entities): ParseResult => { 5 | if (!Array.isArray(entities) || text == null) { 6 | return emptyParseResult; 7 | } 8 | return { ...emptyParseResult, text, entities }; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/theme/react.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { type Theme } from '@boluo/types'; 3 | import { getThemeFromDom, observeTheme } from './index'; 4 | 5 | export const useTheme = (): Theme => { 6 | const [theme, setTheme] = useState(getThemeFromDom()); 7 | 8 | useEffect(() => { 9 | return observeTheme(setTheme); 10 | }, []); 11 | 12 | return theme; 13 | }; 14 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/Text.tsx: -------------------------------------------------------------------------------- 1 | import styled from '@emotion/styled'; 2 | import { spacingN, textBase } from '../../styles/atoms'; 3 | import { textColor } from '../../styles/colors'; 4 | 5 | export const Text = styled.p` 6 | color: ${textColor}; 7 | font-size: ${textBase}; 8 | margin: 0; 9 | line-height: 1.75rem; 10 | padding: ${spacingN(1)} 0; 11 | `; 12 | 13 | export default Text; 14 | -------------------------------------------------------------------------------- /apps/spa/components/ChatNotFound.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { PaneList } from './PaneList'; 3 | 4 | export const ChatNotFound = () => { 5 | const defaultPane = useMemo(() => { 6 | return ( 7 |
Not found
8 | ); 9 | }, []); 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /docs/api/Users/Edit User.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Edit User 3 | type: http 4 | seq: 10 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/edit 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "nickname": "石破天惊lovelove", 16 | "bio": "God is in his heaven, all is right with the world", 17 | "avatar": null, 18 | "defaultColor": "preset:blue" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/hooks/useWebSocketUrl.tsx: -------------------------------------------------------------------------------- 1 | import { apiUrlAtom } from '@boluo/api-browser'; 2 | import { type Atom, atom, useAtomValue } from 'jotai'; 3 | 4 | export const webSocketUrlAtom: Atom = atom((get) => { 5 | const baseUrl = get(apiUrlAtom); 6 | return baseUrl.replace(/^http/, 'ws'); 7 | }); 8 | 9 | export const useWebSocketUrl = () => { 10 | return useAtomValue(webSocketUrlAtom); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/icons/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/corner-down-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/ellipsis-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/more-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/panel-left-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ui/src/entities/EntityExpr.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | import { type ExprEntity } from '@boluo/api'; 3 | import { EntityExprNode } from './EntityExprNode'; 4 | 5 | interface Props { 6 | source: string; 7 | entity: ExprEntity; 8 | level?: number; 9 | } 10 | 11 | export const EntityExpr: FC = ({ entity }) => { 12 | return ; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/utils/src/env.ts: -------------------------------------------------------------------------------- 1 | export const parseBool = (env: string | null | undefined): boolean => { 2 | env = (env ?? 'false').trim().toLowerCase(); 3 | if (env === 'true' || env === '1' || env === 'on') { 4 | return true; 5 | } else if (env === 'false' || env === '0' || env === 'off') { 6 | return false; 7 | } else { 8 | throw new Error(`Invalid boolean value: ${env}`); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/ellipsis.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/storybook/src/LoadingText.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { LoadingText } from '@boluo/ui/LoadingText'; 3 | 4 | const meta: Meta = { 5 | title: 'Feedback/LoadingText', 6 | component: LoadingText, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = { args: {} }; 13 | -------------------------------------------------------------------------------- /apps/storybook/src/OopsKaomoji.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { OopsKaomoji } from '@boluo/ui/OopsKaomoji'; 3 | 4 | const meta: Meta = { 5 | title: 'Feedback/OopsKaomoji', 6 | component: OopsKaomoji, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = { args: {} }; 13 | -------------------------------------------------------------------------------- /packages/icons/icons/key.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/move-vertical.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/paperclip.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/storybook/src/HelpText.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { HelpText } from '@boluo/ui/HelpText'; 3 | 4 | const meta: Meta = { 5 | title: 'Base/HelpText', 6 | component: HelpText, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = { args: { children: 'This is a help text' } }; 13 | -------------------------------------------------------------------------------- /packages/icons/icons/alert-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/user-x.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/icons/icons/x-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/tailwind-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@boluo/tailwind-config", 3 | "version": "0.0.0", 4 | "devDependencies": { 5 | "@boluo/typescript-config": "0.0.0", 6 | "@tailwindcss/postcss": "^4.1.14", 7 | "tailwindcss": "^4.1.14" 8 | }, 9 | "exports": { 10 | ".": "./tailwind.css", 11 | "./postcss": "./postcss.config.js" 12 | }, 13 | "private": true, 14 | "type": "module" 15 | } 16 | -------------------------------------------------------------------------------- /packages/utils/src/random.ts: -------------------------------------------------------------------------------- 1 | export const shuffle = (array: Array): void => { 2 | for (let i = array.length - 1; i > 0; i--) { 3 | const j = Math.floor(Math.random() * (i + 1)); 4 | const temp = array[i]; 5 | array[i] = array[j]!; 6 | array[j] = temp!; 7 | } 8 | }; 9 | export const selectRandom = (array: Array): T => { 10 | return array[Math.floor(Math.random() * array.length)]!; 11 | }; 12 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/filter.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_channels_by_user.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | c AS "channel!: Channel", 3 | cm AS "member!: ChannelMember" 4 | FROM 5 | channel_members cm 6 | INNER JOIN channels c ON cm.channel_id = c.id 7 | AND c.deleted = FALSE 8 | INNER JOIN space_members sm ON cm.user_id = sm.user_id 9 | AND c.space_id = sm.space_id 10 | WHERE 11 | cm.user_id = $1 12 | AND cm.is_joined; 13 | 14 | -------------------------------------------------------------------------------- /apps/spa/hooks/useVirtuosoRef.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { createContext, type RefObject } from 'react'; 3 | import type { VirtuosoHandle } from 'react-virtuoso'; 4 | 5 | export const VirtuosoRefContext = createContext>({ 6 | current: null, 7 | }); 8 | 9 | export const useVirtuosoRef = (): RefObject => 10 | useContext(VirtuosoRefContext); 11 | -------------------------------------------------------------------------------- /packages/icons/icons/arrow-up-wide-short.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/save.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/storybook/src/entities/EntityUnknown.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { EntityUnknown } from '@boluo/ui/entities/EntityUnknown'; 3 | 4 | const meta: Meta = { 5 | title: 'Entities/Unknown', 6 | component: EntityUnknown, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = {}; 13 | -------------------------------------------------------------------------------- /packages/icons/icons/arrow-down-wide-short.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/triangle-alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ui/src/entities/EntityEvaluatedExpr.tsx: -------------------------------------------------------------------------------- 1 | import { type FC } from 'react'; 2 | import { EntityExprNode } from './EntityExprNode'; 3 | import { type EvaluatedExpr } from '@boluo/api'; 4 | 5 | interface Props { 6 | source: string; 7 | entity: EvaluatedExpr; 8 | level?: number; 9 | } 10 | 11 | export const EntityEvaluatedExpr: FC = ({ entity }) => { 12 | return ; 13 | }; 14 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_channel_member.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | cm AS "member!: ChannelMember" 3 | FROM 4 | channel_members cm 5 | INNER JOIN channels ch ON cm.channel_id = ch.id 6 | AND ch.deleted = FALSE 7 | INNER JOIN space_members sm ON ch.space_id = sm.space_id 8 | AND cm.user_id = sm.user_id 9 | WHERE 10 | cm.user_id = $1 11 | AND cm.channel_id = $2 12 | AND cm.is_joined 13 | LIMIT 1; 14 | 15 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_member_by_channel.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | cm AS "channel!: ChannelMember", 3 | sm AS "space!: SpaceMember" 4 | FROM 5 | channel_members cm 6 | INNER JOIN channels ch ON cm.channel_id = ch.id 7 | AND ch.deleted = FALSE 8 | INNER JOIN space_members sm ON sm.space_id = ch.space_id 9 | AND sm.user_id = cm.user_id 10 | WHERE 11 | cm.channel_id = $1 12 | AND cm.is_joined; 13 | 14 | -------------------------------------------------------------------------------- /apps/server/sql/spaces/edit.sql: -------------------------------------------------------------------------------- 1 | UPDATE 2 | spaces 3 | SET 4 | name = COALESCE($2, name), 5 | description = COALESCE($3, description), 6 | default_dice_type = COALESCE($4, default_dice_type), 7 | explorable = COALESCE($5, explorable), 8 | is_public = COALESCE($6, is_public), 9 | allow_spectator = COALESCE($7, allow_spectator) 10 | WHERE 11 | id = $1 12 | RETURNING 13 | spaces AS "space!: Space"; 14 | 15 | -------------------------------------------------------------------------------- /packages/icons/icons/clipboard.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/cloud-off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/satellite-dish.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/upload.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/locale/src/server.ts: -------------------------------------------------------------------------------- 1 | import { createIntl, type IntlShape } from '@formatjs/intl'; 2 | import { onIntlError, toLocale } from '.'; 3 | import { loadMessages } from './dynamic'; 4 | 5 | export const getIntl = async ({ lang }: { lang: string }): Promise => { 6 | const locale = toLocale(lang); 7 | const messages = await loadMessages(locale); 8 | return createIntl({ locale, messages, onError: onIntlError }); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/ui/src/chat/IsActionIndicator.tsx: -------------------------------------------------------------------------------- 1 | import { PersonRunning } from '@boluo/icons'; 2 | import { Delay } from '../Delay'; 3 | 4 | export const IsActionIndicator = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | Action 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.sqlx/query-ff1f5b18915188e185f0deef5242e09653b3b2bc8623422e549b529b7887cee3.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE user_sessions\nSET active = FALSE\nWHERE id = $1;\n", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "ff1f5b18915188e185f0deef5242e09653b3b2bc8623422e549b529b7887cee3" 14 | } 15 | -------------------------------------------------------------------------------- /packages/icons/icons/a-arrow-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/log-in.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/log-out.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/scaling.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/paper-plane.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /apps/legacy/src/components/chat/compose/BroadcastAreClosed.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/react'; 2 | import React from 'react'; 3 | 4 | interface Props { 5 | className?: string; 6 | } 7 | 8 | const style = css` 9 | font-style: italic; 10 | `; 11 | 12 | export const BroadcastAreClosed = ({ className }: Props) => { 13 | return ( 14 | 15 | [预览广播已关闭] 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /apps/server/text/email-change/content.en.html: -------------------------------------------------------------------------------- 1 |

Hello!

2 |

You have requested to change your email address for your Boluo account. Please click the link below to confirm this change:

3 |

Confirm Email Change

4 |

This link will expire in 24 hours.

5 |

If you didn't request this email change, please ignore this email and your current email address will remain unchanged.

6 | -------------------------------------------------------------------------------- /apps/site/src/app/[lang]/[theme]/account/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from 'react'; 2 | import { BackLink } from '../../../../components/BackLink'; 3 | 4 | export default function Layout({ children }: { children: ReactNode }) { 5 | return ( 6 |
7 | 8 | {children} 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /apps/spa/state/view.utils.ts: -------------------------------------------------------------------------------- 1 | import { type Pane, type PaneData } from './view.types'; 2 | 3 | export const findPane = (panes: Pane[], predicate: (pane: PaneData) => boolean) => { 4 | for (const pane of panes) { 5 | if (predicate(pane)) { 6 | return pane; 7 | } 8 | const childPane = pane.child?.pane; 9 | if (childPane && predicate(childPane)) { 10 | return childPane; 11 | } 12 | } 13 | return null; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/icons/icons/alert-triangle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/ErrorMessageBox.tsx: -------------------------------------------------------------------------------- 1 | import { type ReactNode } from 'react'; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | } 6 | 7 | export const ErrorMessageBox = ({ children }: Props) => { 8 | return ( 9 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.sqlx/query-d3323ec6958d941643ea3121a8e35ab44420baf50708e62a3f920e807ec07c1a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE\n spaces\nSET\n deleted = TRUE\nWHERE\n id = $1;\n\n", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "d3323ec6958d941643ea3121a8e35ab44420baf50708e62a3f920e807ec07c1a" 14 | } 15 | -------------------------------------------------------------------------------- /apps/interpreter-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "interpreter-cli", 3 | "version": "0.0.0", 4 | "dependencies": { 5 | "@boluo/interpreter": "0.0.0" 6 | }, 7 | "devDependencies": { 8 | "@types/node": "^24.0.12", 9 | "tsx": "^4.21.0", 10 | "typescript": "^5.9.3" 11 | }, 12 | "private": true, 13 | "scripts": { 14 | "check": "tsc --noEmit", 15 | "start": "tsx src/index.ts" 16 | }, 17 | "type": "module" 18 | } 19 | -------------------------------------------------------------------------------- /packages/icons/icons/help-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/shared-types/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shared-types" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | [lib] 7 | name = "shared_types" 8 | 9 | [dependencies] 10 | serde = { version = "1", features = ["derive"] } 11 | serde_json = "1" 12 | specta = { version = "2.0.0-rc.22", features = [ 13 | "chrono", 14 | "derive", 15 | "export", 16 | "serde", 17 | "serde_json", 18 | "uuid", 19 | ] } 20 | specta-typescript = "0.0.9" 21 | -------------------------------------------------------------------------------- /packages/ui/src/entities/EntityStrong.tsx: -------------------------------------------------------------------------------- 1 | import { type EntityOf } from '@boluo/api'; 2 | import type { FC } from 'react'; 3 | 4 | interface Props { 5 | source: string; 6 | entity: EntityOf<'Strong'>; 7 | } 8 | 9 | export const EntityStrong: FC = ({ 10 | source, 11 | entity: { 12 | child: { start, len }, 13 | }, 14 | }) => { 15 | return {source.substring(start, start + len)}; 16 | }; 17 | -------------------------------------------------------------------------------- /.sqlx/query-0e33a532c76682fe608925c12632c97a3b7362910f02bba15b778f900d650cc0.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE\n messages\nSET\n deleted = TRUE\nWHERE\n id = $1;\n\n", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "0e33a532c76682fe608925c12632c97a3b7362910f02bba15b778f900d650cc0" 14 | } 15 | -------------------------------------------------------------------------------- /.sqlx/query-d30e7b47ad997e47163b89e21fcf7a5ae0a159fb74f492c72d5fbdfa27e344d6.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE\n users\nSET\n deactivated = TRUE\nWHERE\n id = $1;\n\n", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid" 9 | ] 10 | }, 11 | "nullable": [] 12 | }, 13 | "hash": "d30e7b47ad997e47163b89e21fcf7a5ae0a159fb74f492c72d5fbdfa27e344d6" 14 | } 15 | -------------------------------------------------------------------------------- /docs/api/Channels/By Space.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: By Space 3 | type: http 4 | seq: 2 5 | } 6 | 7 | get { 8 | url: {{base_url}}/channels/by_space?id={{space_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | id: {{space_id}} 15 | } 16 | 17 | script:post-response { 18 | let data = res.getBody(); 19 | if (data.isOk && data.ok.length > 0) { 20 | bru.setVar("channel_id", data.ok[0].channel.id); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/icons/icons/external-link.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/shrink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/tool.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ui/src/entities/RollBox.tsx: -------------------------------------------------------------------------------- 1 | import { type FC, type ReactNode } from 'react'; 2 | 3 | interface Props { 4 | children: ReactNode; 5 | } 6 | 7 | export const RollBox: FC = ({ children }) => { 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.env.local.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:5432/boluo?sslmode=disable 2 | REDIS_URL=redis://127.0.0.1:6379 3 | BACKEND_URL=http://127.0.0.1:3033 4 | APP_URL=http://127.0.0.1:3200 5 | SITE_URL=http://127.0.0.1:3100 6 | PUBLIC_MEDIA_URL=http://127.0.0.1:9000/boluo 7 | 8 | SECRET=SOME_SECRET 9 | S3_ACCESS_KEY_ID=boluo 10 | S3_SECRET_ACCESS_KEY=boluo-development 11 | S3_ENDPOINT_URL=http://127.0.0.1:9000 12 | S3_BUCKET_NAME=boluo 13 | -------------------------------------------------------------------------------- /apps/spa/hooks/useChannel.tsx: -------------------------------------------------------------------------------- 1 | import { type Channel } from '@boluo/api'; 2 | import React from 'react'; 3 | import { recordWarn } from '../error'; 4 | 5 | export const ChannelContext = React.createContext(null); 6 | 7 | export const useChannel = () => { 8 | const channel = React.useContext(ChannelContext); 9 | if (!channel) { 10 | recordWarn('useChannel must be used within a channel context'); 11 | } 12 | return channel; 13 | }; 14 | -------------------------------------------------------------------------------- /apps/storybook/src/Kbd.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { Kbd } from '@boluo/ui/Kbd'; 3 | 4 | const meta: Meta = { 5 | title: 'Base/Kbd', 6 | component: Kbd, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = { args: { children: 'Ctrl' } }; 13 | 14 | export const Small: Story = { args: { children: 'Ctrl', variant: 'small' } }; 15 | -------------------------------------------------------------------------------- /apps/storybook/src/SomethingWentWrong.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { SomethingWentWrong } from '@boluo/ui/SomethingWentWrong'; 3 | 4 | const meta: Meta = { 5 | title: 'Feedback/SomethingWentWrong', 6 | component: SomethingWentWrong, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = { args: {} }; 13 | -------------------------------------------------------------------------------- /apps/storybook/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | import tailwindcss from '@tailwindcss/vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | tailwindcss(), 8 | react({ 9 | babel: { 10 | plugins: [ 11 | ['formatjs', { idInterpolationPattern: '[sha512:contenthash:base64:6]', ast: true }], 12 | ], 13 | }, 14 | }), 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /docs/api/Users/Login.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: Login 3 | type: http 4 | seq: 2 5 | } 6 | 7 | post { 8 | url: {{base_url}}/users/login 9 | body: json 10 | auth: inherit 11 | } 12 | 13 | body:json { 14 | { 15 | "username": "bruno", 16 | "password": "BrunoBruno", 17 | "withToken": true 18 | } 19 | } 20 | 21 | script:post-response { 22 | let data = res.getBody(); 23 | let token = bru.setEnvVar("access_token", data.ok.token); 24 | } 25 | -------------------------------------------------------------------------------- /packages/icons/icons/hash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/split-horizontal.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/utils/src/flags.ts: -------------------------------------------------------------------------------- 1 | import { parseBool } from './env.js'; 2 | 3 | const getBoolFlag = (key: string, defaultValue = false): boolean => { 4 | if (globalThis.window?.localStorage) { 5 | const storage = globalThis.window.localStorage; 6 | const value = storage.getItem(key.toLowerCase()); 7 | return parseBool(value); 8 | } else { 9 | return defaultValue; 10 | } 11 | }; 12 | 13 | export const IS_DEBUG = getBoolFlag('BOLUO_DEBUG'); 14 | -------------------------------------------------------------------------------- /apps/legacy/src/components/atoms/Delay.tsx: -------------------------------------------------------------------------------- 1 | import { type MeasureResult } from '../../hooks/useBaseUrlDelay'; 2 | 3 | export const Delay = ({ delay }: { delay: MeasureResult | 'LOADING' }) => { 4 | if (delay === 'LOADING') { 5 | return ...; 6 | } else if (delay === 'ERROR') { 7 | return 出错; 8 | } else if (delay === 'TIMEOUT') { 9 | return 超时; 10 | } 11 | return {delay.toFixed(2)}ms; 12 | }; 13 | -------------------------------------------------------------------------------- /apps/server/src/events/api.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use uuid::Uuid; 3 | 4 | #[derive(Deserialize, Serialize, specta::Type)] 5 | pub struct Token { 6 | pub token: Uuid, 7 | } 8 | 9 | #[derive(Deserialize, Serialize, Default, specta::Type)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct MakeToken { 12 | #[serde(default)] 13 | pub space_id: Option, 14 | #[serde(default)] 15 | pub user_id: Option, 16 | } 17 | -------------------------------------------------------------------------------- /packages/icons/icons/globe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/db/README.md: -------------------------------------------------------------------------------- 1 | # Database Configuration 2 | 3 | ## Schema 4 | 5 | ``` 6 | pg_dump --schema-only -U postgres --no-owner --no-privileges boluo > db/schema.sql 7 | ``` 8 | 9 | ## Dump 10 | 11 | ``` 12 | pg_dump --username ... --host ... --dbname boluo --no-owner --no-privileges --schema public --file boluo-$(date +%Y%m%d).dump 13 | ``` 14 | 15 | ## Restore 16 | 17 | ``` 18 | psql --username ... --host ... --dbname boluo --file boluo-$(date +%Y%m%d).dump 19 | ``` 20 | -------------------------------------------------------------------------------- /apps/storybook/src/Loading.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { Loading } from '@boluo/ui/Loading'; 3 | 4 | const meta: Meta = { 5 | title: 'Feedback/Loading', 6 | component: Loading, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Inline: Story = { args: { type: 'inline' } }; 13 | 14 | export const Block: Story = { args: { type: 'block' } }; 15 | -------------------------------------------------------------------------------- /packages/hooks/useDetectBrowserSupport.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | export const useDetectBrowserSupport = () => { 4 | const [isSupported, setIsSupported] = useState(true); 5 | useEffect(() => { 6 | // setIsSupported(false); 7 | setIsSupported( 8 | CSS.supports('container-type', 'inline-size') && 9 | CSS.supports('grid-template-rows', 'subgrid'), 10 | ); 11 | }, []); 12 | return isSupported; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/icons/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /packages/icons/icons/user-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/plus-circle.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /apps/server/sql/messages/get.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | msg AS "message!: Message", 3 | (msg.whisper_to_users IS NOT NULL 4 | AND cm.is_master IS NOT TRUE 5 | AND ($2 IS NULL 6 | OR $2 <> ALL (msg.whisper_to_users))) AS "should_hide!" 7 | FROM 8 | messages msg 9 | LEFT JOIN channel_members cm ON cm.channel_id = msg.channel_id 10 | AND cm.user_id = $2 11 | WHERE 12 | msg.id = $1 13 | AND msg.deleted = FALSE 14 | LIMIT 1; 15 | 16 | -------------------------------------------------------------------------------- /apps/storybook/src/DiceSelect.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { DiceSelect } from '@boluo/ui/DiceSelect'; 3 | import { fn } from 'storybook/test'; 4 | 5 | const meta: Meta = { 6 | title: 'Derived/DiceSelect', 7 | component: DiceSelect, 8 | }; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Basic: Story = { args: { value: 'd20', onChange: fn() } }; 14 | -------------------------------------------------------------------------------- /apps/storybook/src/entities/EntityExprUnknown.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { EntityExprNodeUnknown } from '@boluo/ui/entities/EntityExprUnknown'; 3 | 4 | const meta: Meta = { 5 | title: 'Entities/Expr/Unknown', 6 | component: EntityExprNodeUnknown, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = {}; 13 | -------------------------------------------------------------------------------- /packages/icons/icons/thumbs-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/users.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.sqlx/query-f79ae2c952ff6125556afa20b2f55d44e304fe66877f45d2a61a9f77b1d86b0a.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "DELETE FROM space_members\nWHERE user_id = $1\n AND space_id = $2;\n\n", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Uuid" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "f79ae2c952ff6125556afa20b2f55d44e304fe66877f45d2a61a9f77b1d86b0a" 15 | } 16 | -------------------------------------------------------------------------------- /apps/server/sql/channels/get_with_space_member.sql: -------------------------------------------------------------------------------- 1 | SELECT 2 | cm AS "channel!: ChannelMember", 3 | sm AS "space!: SpaceMember" 4 | FROM 5 | channel_members cm 6 | INNER JOIN channels ch ON cm.channel_id = ch.id 7 | AND ch.deleted = FALSE 8 | INNER JOIN space_members sm ON sm.space_id = ch.space_id 9 | AND sm.user_id = cm.user_id 10 | WHERE 11 | cm.user_id = $1 12 | AND cm.channel_id = $2 13 | AND cm.is_joined 14 | LIMIT 1; 15 | 16 | -------------------------------------------------------------------------------- /apps/storybook/src/ErrorMessageBox.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { ErrorMessageBox } from '@boluo/ui/ErrorMessageBox'; 3 | 4 | const meta: Meta = { 5 | title: 'Feedback/ErrorMessageBox', 6 | component: ErrorMessageBox, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = { args: { children: 'This is an error message' } }; 13 | -------------------------------------------------------------------------------- /packages/icons/icons/thumbs-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/chevron-down.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/chevron-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /apps/legacy/src/assets/icons/comment-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/icons/icons/scroll-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.sqlx/query-da12d4699b27ab86418abc9143bd0eb319f127c73112a0cbe4349213db435632.json: -------------------------------------------------------------------------------- 1 | { 2 | "db_name": "PostgreSQL", 3 | "query": "UPDATE\n users\nSET\n PASSWORD = crypt($2, gen_salt('bf'))\nWHERE\n id = $1;\n\n", 4 | "describe": { 5 | "columns": [], 6 | "parameters": { 7 | "Left": [ 8 | "Uuid", 9 | "Text" 10 | ] 11 | }, 12 | "nullable": [] 13 | }, 14 | "hash": "da12d4699b27ab86418abc9143bd0eb319f127c73112a0cbe4349213db435632" 15 | } 16 | -------------------------------------------------------------------------------- /packages/icons/icons/file-plus.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/mask.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/ui/src/FallbackIcon.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from 'react'; 2 | 3 | interface Props { 4 | placeholder?: string; 5 | className?: string; 6 | } 7 | 8 | export const FallbackIcon = memo( 9 | ({ placeholder = '⍰', className = 'inline-block w-[1em] h-[1em] text-text-subtle' }: Props) => { 10 | return ( 11 | 12 | {placeholder} 13 | 14 | ); 15 | }, 16 | ); 17 | FallbackIcon.displayName = 'FallbackIcon'; 18 | -------------------------------------------------------------------------------- /packages/ui/src/LampSwitch.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export const LampSwitch = ({ isOn }: { isOn: boolean }) => { 4 | return ( 5 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/ui/src/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { CircleNotch } from '@boluo/icons'; 3 | import React from 'react'; 4 | import Icon from './Icon'; 5 | 6 | interface Props { 7 | className?: string; 8 | label?: string; 9 | } 10 | 11 | export const Spinner: React.FC = ({ label, className }) => { 12 | return ( 13 | 14 | ); 15 | }; 16 | Spinner.displayName = 'Spinner'; 17 | -------------------------------------------------------------------------------- /apps/storybook/src/FloatingBox.stories.ts: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | 3 | import { FloatingBox } from '@boluo/ui/FloatingBox'; 4 | 5 | const meta: Meta = { 6 | title: 'Base/FloatingBox', 7 | component: FloatingBox, 8 | args: { 9 | className: 'p-4', 10 | }, 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const Basic: Story = { args: { children: 'This is a floating box' } }; 17 | -------------------------------------------------------------------------------- /packages/hooks/useQueryChannel.tsx: -------------------------------------------------------------------------------- 1 | import type { ApiError, Channel } from '@boluo/api'; 2 | import { get } from '@boluo/api-browser'; 3 | import useSWR, { type SWRResponse } from 'swr'; 4 | import { unwrap } from '@boluo/utils/result'; 5 | 6 | export const useQueryChannel = (channelId: string): SWRResponse => { 7 | const key = ['/channels/query', channelId] as const; 8 | return useSWR(key, ([path, id]) => get(path, { id }).then(unwrap)); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/icons/icons/book-copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/dice.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/icons/icons/shuffle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/legacy/src/api/media.ts: -------------------------------------------------------------------------------- 1 | import { type Id } from '../utils/id'; 2 | 3 | export interface Media { 4 | id: Id; 5 | mimeType: string; 6 | uploaderId: Id; 7 | filename: string; 8 | originalFilename: string; 9 | hash: string; 10 | description: string; 11 | created: number; 12 | } 13 | 14 | export interface PreSign { 15 | filename: string; 16 | mimeType: string; 17 | size: number; 18 | } 19 | 20 | export interface PreSignResult { 21 | url: string; 22 | mediaId: string; 23 | } 24 | -------------------------------------------------------------------------------- /apps/site/src/app/[lang]/[theme]/account/sign-up/Footer.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | import ChevronLeft from '@boluo/icons/ChevronLeft'; 3 | import { FormattedMessage } from 'react-intl'; 4 | import { ButtonLink } from '../../../../../components/ButtonLink'; 5 | 6 | export function Footer() { 7 | return ( 8 |
9 | 10 | 11 | 12 | 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/spa/hooks/useChatContainerClassnames.tsx: -------------------------------------------------------------------------------- 1 | import { type Settings } from '@boluo/settings'; 2 | import { useSettings } from './useSettings'; 3 | import clsx from 'clsx'; 4 | 5 | export const settingsToClassnames = (settings: Partial) => { 6 | return clsx(settings.layout ?? 'irc-layout', settings.messageSize, settings.inGameFont); 7 | }; 8 | 9 | export const useChatContainerClassnames = () => { 10 | const settings = useSettings(); 11 | return settingsToClassnames(settings); 12 | }; 13 | -------------------------------------------------------------------------------- /apps/storybook/src/BroadcastTurnedOff.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { BroadcastTurnedOff } from '@boluo/ui/chat/BroadcastTurnedOff'; 3 | 4 | const meta: Meta = { 5 | title: 'Chat/BroadcastTurnedOff', 6 | component: BroadcastTurnedOff, 7 | }; 8 | 9 | export default meta; 10 | type Story = StoryObj; 11 | 12 | export const Basic: Story = { 13 | args: { 14 | count: 0, 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /apps/storybook/src/IsActionIndicator.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react-vite'; 2 | import { IsActionIndicator } from '@boluo/ui/chat/IsActionIndicator'; 3 | 4 | const meta: Meta = { 5 | title: 'Chat/IsActionIndicator', 6 | component: IsActionIndicator, 7 | parameters: { 8 | layout: 'centered', 9 | }, 10 | }; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const Basic: Story = {}; 16 | -------------------------------------------------------------------------------- /docs/api/Messages/By Channel.bru: -------------------------------------------------------------------------------- 1 | meta { 2 | name: By Channel 3 | type: http 4 | seq: 1 5 | } 6 | 7 | get { 8 | url: {{base_url}}/messages/by_channel?channelId={{channel_id}} 9 | body: none 10 | auth: inherit 11 | } 12 | 13 | params:query { 14 | channelId: {{channel_id}} 15 | before: 16 | limit: 50 17 | } 18 | 19 | script:post-response { 20 | let data = res.getBody(); 21 | if (data.isOk && data.ok.length > 0) { 22 | bru.setVar("message_id", data.ok[0].id); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/icons/icons/trash.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | --------------------------------------------------------------------------------