├── .deepsource.toml ├── .github ├── FUNDING.yml └── workflows │ ├── deploy.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── client ├── args │ └── args.go ├── cli │ ├── cli.go │ ├── cmd │ │ ├── cmd.go │ │ ├── connect │ │ │ └── connect.go │ │ ├── help │ │ │ └── help.go │ │ ├── identity │ │ │ ├── create.go │ │ │ └── identity.go │ │ └── version │ │ │ └── version.go │ └── identity │ │ └── create.go ├── config │ ├── config.go │ ├── default.go │ ├── file.go │ └── identity.go ├── errs │ └── error.go ├── format │ ├── error.go │ ├── format.go │ ├── join.go │ ├── leave.go │ ├── message.go │ ├── motd.go │ └── notice.go ├── identity │ ├── create.go │ ├── entity.go │ ├── identity.go │ └── load.go ├── markdown │ ├── bold.go │ └── markdown.go ├── tui │ ├── chatbox │ │ └── chatbox.go │ ├── prompt │ │ ├── mode_cmd.go │ │ ├── mode_msg.go │ │ ├── modes.go │ │ ├── prompt.go │ │ └── util.go │ ├── statusline │ │ └── statusline.go │ ├── tui.go │ └── util │ │ ├── util.go │ │ └── util_test.go └── version.go ├── cmd ├── client │ └── client.go └── server │ └── server.go ├── conf ├── docker │ └── docker-compose.yml └── linux │ ├── runit │ └── boltchat │ │ └── run │ └── systemd │ └── boltchat.service ├── docs ├── CODE_OF_CONDUCT.md ├── installation.md ├── protocol-spec.md └── quick-start.md ├── go.mod ├── go.sum ├── lib ├── client │ ├── client.go │ ├── command.go │ ├── events.go │ ├── message.go │ ├── options.go │ └── util.go └── pgp │ ├── armor.go │ ├── create.go │ └── load.go ├── magefile.go ├── protocol ├── errs │ └── errors.go ├── events │ ├── command.go │ ├── error.go │ ├── event.go │ ├── join.go │ ├── leave.go │ ├── message.go │ ├── motd.go │ └── notice.go ├── message.go ├── user.go └── version.go ├── scripts └── entrypoint.sh ├── server ├── commands │ ├── commands.go │ └── ping.go ├── handlers │ ├── command.go │ ├── connect.go │ ├── default.go │ ├── handlers.go │ ├── join.go │ └── message.go ├── listener.go ├── logging │ └── logger.go ├── pgp │ └── verify.go ├── plugins │ ├── manager.go │ ├── nickname_val.go │ ├── plugins.go │ └── rate_limiter.go ├── pools │ ├── connection.go │ └── connpool.go └── util │ └── banner.go └── util └── version ├── format.go └── version.go /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "shell" 5 | enabled = true 6 | 7 | [[analyzers]] 8 | name = "go" 9 | enabled = true 10 | 11 | [analyzers.meta] 12 | import_root = "github.com/boltchat/bolt" 13 | dependencies_vendored = false -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: keesvv 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | create-release: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | assets_upload_url: ${{ steps.create_release.outputs.upload_url }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Create Release 17 | id: create_release 18 | uses: actions/create-release@v1 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | with: 22 | tag_name: ${{ github.ref }} 23 | release_name: Release ${{ github.ref }} 24 | body: | 25 | Changelog: 26 | - @@ TO BE FILLED OUT @@ 27 | draft: true 28 | prerelease: true 29 | 30 | build-binaries: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | - name: Set up the environment for Go 36 | uses: actions/setup-go@v2 37 | with: 38 | go-version: '1.15.6' 39 | - name: Install dependencies 40 | run: | 41 | go get github.com/magefile/mage 42 | go install github.com/magefile/mage 43 | - name: Compile binaries 44 | run: mage build:all 45 | - name: Compress binaries 46 | run: mage ci:compressBinaries 47 | - name: Save binaries as artifact 48 | uses: actions/upload-artifact@v2 49 | with: 50 | name: binaries 51 | path: binaries.tar.gz 52 | 53 | upload-assets: 54 | runs-on: ubuntu-latest 55 | needs: [create-release, build-binaries] 56 | steps: 57 | - name: Download binary artifacts 58 | uses: actions/download-artifact@v2 59 | with: 60 | name: binaries 61 | - name: Extract artifacts 62 | run: tar xvf binaries.tar.gz 63 | 64 | - name: Upload Linux/AMD64 client binary 65 | uses: actions/upload-release-asset@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | upload_url: ${{ needs.create-release.outputs.assets_upload_url }} 70 | asset_path: build/boltchat-client-linux-amd64 71 | asset_name: boltchat-client-linux-amd64 72 | asset_content_type: application/octet-stream 73 | 74 | - name: Upload Windows/AMD64 client binary 75 | uses: actions/upload-release-asset@v1 76 | env: 77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | with: 79 | upload_url: ${{ needs.create-release.outputs.assets_upload_url }} 80 | asset_path: build/boltchat-client-windows-amd64.exe 81 | asset_name: boltchat-client-windows-amd64.exe 82 | asset_content_type: application/octet-stream 83 | 84 | - name: Upload Darwin/AMD64 client binary 85 | uses: actions/upload-release-asset@v1 86 | env: 87 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 88 | with: 89 | upload_url: ${{ needs.create-release.outputs.assets_upload_url }} 90 | asset_path: build/boltchat-client-darwin-amd64 91 | asset_name: boltchat-client-darwin-amd64 92 | asset_content_type: application/octet-stream 93 | 94 | - name: Upload Linux/AMD64 server binary 95 | uses: actions/upload-release-asset@v1 96 | env: 97 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 98 | with: 99 | upload_url: ${{ needs.create-release.outputs.assets_upload_url }} 100 | asset_path: build/boltchat-server-linux-amd64 101 | asset_name: boltchat-server-linux-amd64 102 | asset_content_type: application/octet-stream 103 | 104 | - name: Upload Windows/AMD64 server binary 105 | uses: actions/upload-release-asset@v1 106 | env: 107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 | with: 109 | upload_url: ${{ needs.create-release.outputs.assets_upload_url }} 110 | asset_path: build/boltchat-server-windows-amd64.exe 111 | asset_name: boltchat-server-windows-amd64.exe 112 | asset_content_type: application/octet-stream 113 | 114 | - name: Upload Darwin/AMD64 server binary 115 | uses: actions/upload-release-asset@v1 116 | env: 117 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 118 | with: 119 | upload_url: ${{ needs.create-release.outputs.assets_upload_url }} 120 | asset_path: build/boltchat-server-darwin-amd64 121 | asset_name: boltchat-server-darwin-amd64 122 | asset_content_type: application/octet-stream 123 | 124 | docker: 125 | runs-on: ubuntu-latest 126 | steps: 127 | - name: Checkout 128 | uses: actions/checkout@v2 129 | 130 | - name: Set up QEMU 131 | uses: docker/setup-qemu-action@v1 132 | 133 | - name: Set up Docker Buildx 134 | uses: docker/setup-buildx-action@v1 135 | 136 | - name: Login to GitHub Packages 137 | uses: docker/login-action@v1 138 | with: 139 | username: ${{ secrets.DOCKERHUB_USERNAME }} 140 | password: ${{ secrets.DOCKERHUB_TOKEN }} 141 | 142 | - name: Build and push 143 | uses: docker/build-push-action@v2 144 | with: 145 | context: . 146 | file: ./Dockerfile 147 | platforms: linux/amd64 148 | push: true 149 | tags: boltchat/server:latest 150 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Set up the environment for Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: '1.15.6' 15 | - name: Install dependencies 16 | run: go get -u golang.org/x/lint/golint 17 | - name: Run linter 18 | run: golint . 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | unit: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v2 11 | - name: Set up the environment for Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: '1.15.6' 15 | - name: Install dependencies 16 | run: | 17 | go get github.com/magefile/mage 18 | go install github.com/magefile/mage 19 | - name: Run unit tests 20 | run: mage test:unit 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binary outputs 2 | build/ 3 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "emeraldwalk.runonsave": { 3 | "commands": [{ 4 | "cmd": "mage license", 5 | "match": ".go" 6 | }] 7 | } 8 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v0.2.0-alpha 2 | 3 | ### Features 4 | - PGP authentication & message signing 5 | - Server plugins 6 | - Rate limiting plugin 7 | - Nickname validation plugin 8 | - A command-line interface 9 | 10 | ### Improvements 11 | - CTRL+L shortcut for clearing the buffer 12 | - Better logging 13 | - Message timestamps are now generated by the server, not the client 14 | - `runit` & `systemd` services 15 | 16 | ### Fixes 17 | - Clients crashing when events were received at a fast pace 18 | 19 | ## v0.1.0-alpha 20 | 21 | This is the very first prerelease of the Bolt protocol, client and server. Do not expect too much from it yet, but a start has been made. 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build stage 2 | FROM golang:1.15.6-alpine AS build 3 | 4 | ## Git is apparently needed for Mage 5 | RUN apk add git 6 | 7 | RUN go get github.com/magefile/mage && \ 8 | go install github.com/magefile/mage 9 | 10 | WORKDIR /src 11 | COPY . . 12 | 13 | ## Compile the static server binary 14 | RUN mage build:serverStatic 15 | 16 | # Deploy stage 17 | FROM busybox:1.32.1 18 | WORKDIR /app 19 | COPY --from=build /src/build/boltchat-server-linux-amd64 ./server 20 | COPY scripts/entrypoint.sh /entrypoint.sh 21 | 22 | ## Executable permissions 23 | RUN chmod +x /entrypoint.sh 24 | 25 | ENTRYPOINT ["/entrypoint.sh"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![boltchat](https://raw.githubusercontent.com/boltchat/branding/main/svg/bolt-banner.svg) 2 | > ⚡ A fast, lightweight, and secure chat protocol, client and server, written in Go. 3 | 4 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/boltchat/bolt/Deploy?label=deploy) 5 | ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/boltchat/bolt/Test?label=test) 6 | ![CodeFactor Grade](https://img.shields.io/codefactor/grade/github/boltchat/bolt/develop) 7 | 8 | ## About 9 | > ⚠ IMPORTANT: This project is still a work-in-progress. I strongly discourage installing this on a 10 | > public-facing server as it could potentially harm your security and privacy. See the 11 | > [roadmap](https://github.com/boltchat/bolt/projects) for the progress of this project. 12 | 13 | _Bolt_ is intended as a modern replacement for [IRC](https://en.wikipedia.org/wiki/Internet_Relay_Chat). 14 | I started this project because I feel like there aren't many open source chat protocols that follow modern 15 | standards. 16 | 17 | Not only do I think it's a great fit for an IRC replacement; it might even be suitable for a replacement of 18 | present-day proprietary protocols and chat applications, such as Discord and Slack. _Bolt_ comes with 19 | a nifty text-based user interface, but since it uses its own protocol, it's possible to build a GUI client 20 | in, say, Electron. (please don't, use [Tauri](https://github.com/tauri-apps/tauri)) 21 | 22 | ## Roadmap 23 | The project boards for _Bolt_ can be found [here](https://github.com/boltchat/bolt/projects). 24 | 25 | ## References 26 | * [Installation guide](./docs/installation.md) 27 | * [Quick start guide](./docs/quick-start.md) 28 | * [Protocol Specification](./docs/protocol-spec.md) 29 | 30 | ## Author 31 | [Kees van Voorthuizen (@keesvv)](https://github.com/keesvv) 32 | 33 | ## Branding 34 | The branding for this project can be found [in this repository](https://github.com/boltchat/branding). 35 | 36 | ## License 37 | This project is licensed under the [Apache License 2.0](./LICENSE). 38 | -------------------------------------------------------------------------------- /client/args/args.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package args 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "strconv" 21 | "strings" 22 | 23 | "github.com/boltchat/client/config" 24 | "github.com/boltchat/client/errs" 25 | "github.com/fatih/color" 26 | ) 27 | 28 | type Args struct { 29 | Hostname string 30 | Port int 31 | Identity string 32 | } 33 | 34 | func printUsage() { 35 | fmt.Println("usage: boltchat [identity]") 36 | } 37 | 38 | func GetArgs() *Args { 39 | rawArgs := os.Args[1:] 40 | 41 | // Set identity to 'default' by default 42 | identity := config.DefaultIdentity 43 | 44 | if len(rawArgs) < 1 { 45 | printUsage() 46 | os.Exit(1) 47 | } else if len(rawArgs) > 1 { 48 | identity = rawArgs[1] 49 | } 50 | 51 | splitHost := strings.Split(rawArgs[0], ":") 52 | hostname := splitHost[0] 53 | 54 | // The default port 55 | port := 3300 56 | 57 | // Custom port number is specified 58 | if len(splitHost) == 2 { 59 | parsedPort, parseErr := strconv.ParseInt(splitHost[1], 10, 32) 60 | 61 | if parseErr != nil { 62 | errs.Syntax(errs.SyntaxError{ 63 | Error: parseErr, 64 | Desc: fmt.Sprintf( 65 | "You have entered an invalid host. Hosts "+ 66 | "must be in the following format: %s.", 67 | color.HiCyanString("hostname|ip[:port]"), 68 | ), 69 | }) 70 | } 71 | 72 | port = int(parsedPort) 73 | } 74 | 75 | args := &Args{ 76 | Hostname: hostname, 77 | Port: port, 78 | Identity: identity, 79 | } 80 | 81 | return args 82 | } 83 | -------------------------------------------------------------------------------- /client/cli/cli.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cli 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "os" 21 | "strings" 22 | 23 | "github.com/boltchat/client/cli/cmd" 24 | "github.com/boltchat/client/cli/cmd/connect" 25 | "github.com/boltchat/client/cli/cmd/help" 26 | "github.com/boltchat/client/cli/cmd/identity" 27 | "github.com/boltchat/client/cli/cmd/version" 28 | "github.com/fatih/color" 29 | ) 30 | 31 | var ErrTooFewArgs = errors.New("too few arguments") 32 | var ErrCmdNotFound = errors.New("command not found") 33 | var ErrSubCmdNotFound = errors.New("subcommand not found") 34 | 35 | var commands = []*cmd.Command{ 36 | help.HelpCommand, 37 | version.VersionCommand, 38 | connect.ConnectCommand, 39 | identity.IdentityCommand, 40 | } 41 | 42 | func formatCmd(cmd *cmd.Command) string { 43 | return fmt.Sprintf("%s %s\t%s", cmd.Name, cmd.Usage, cmd.Desc) 44 | } 45 | 46 | func PrintUsage() { 47 | fmt.Printf("usage: boltchat [subcommand] [args...]\ncommands:\n") 48 | 49 | for _, cmd := range commands { 50 | if len(cmd.Subcommands) > 0 { 51 | // Print blank line after each group of subcommands 52 | fmt.Println() 53 | } 54 | 55 | // Print command details 56 | fmt.Printf("\t%s\n", formatCmd(cmd)) 57 | 58 | for _, subcmd := range cmd.Subcommands { 59 | // Print subcommand details 60 | fmt.Printf("\t%s %s\n", cmd.Name, formatCmd(subcmd)) 61 | } 62 | } 63 | } 64 | 65 | func getCmd(cmds []*cmd.Command, rawCmd string) *cmd.Command { 66 | for _, cmd := range cmds { 67 | if cmd.Name == strings.ToLower(rawCmd) { 68 | return cmd 69 | } 70 | } 71 | 72 | return nil 73 | } 74 | 75 | func ParseCommand(args []string) (*cmd.Command, error) { 76 | // No command was given 77 | if len(args) == 0 { 78 | return nil, ErrTooFewArgs 79 | } 80 | 81 | // Get the command from the first argument 82 | cmd := getCmd(commands, args[0]) 83 | if cmd == nil { 84 | return nil, ErrCmdNotFound 85 | } 86 | 87 | /* 88 | Print usage when issuing the 'help' command. 89 | This has to be handled here, because the 90 | `commands` array can not reference itself. 91 | */ 92 | if cmd == help.HelpCommand { 93 | cmd.Handler = func(args []string) error { 94 | PrintUsage() 95 | return nil 96 | } 97 | return cmd, nil 98 | } 99 | 100 | // Return the command itself if it doesn't 101 | // have any subcommands 102 | if len(cmd.Subcommands) == 0 { 103 | cmd.Args = args[1:] 104 | return cmd, nil 105 | } 106 | 107 | // No subcommand was given 108 | if len(args) < 2 { 109 | return nil, ErrTooFewArgs 110 | } 111 | 112 | // Get the subcommand from the second argument 113 | subcmd := getCmd(cmd.Subcommands, args[1]) 114 | if subcmd == nil { 115 | return nil, ErrSubCmdNotFound 116 | } 117 | 118 | subcmd.Args = args[2:] 119 | return subcmd, nil 120 | } 121 | 122 | func HandleCommandError(cmdErr error) { 123 | fmt.Printf(color.RedString("Command error: %s\n\n", cmdErr)) 124 | PrintUsage() 125 | os.Exit(1) 126 | } 127 | -------------------------------------------------------------------------------- /client/cli/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package cmd 16 | 17 | type Handler func(args []string) error 18 | 19 | type Command struct { 20 | Name string 21 | Desc string 22 | Usage string 23 | Args []string 24 | Subcommands []*Command 25 | Handler Handler 26 | } 27 | 28 | func (c *Command) Execute() error { 29 | return c.Handler(c.Args) 30 | } 31 | -------------------------------------------------------------------------------- /client/cli/cmd/connect/connect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package connect 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "os" 21 | "strconv" 22 | "strings" 23 | 24 | "github.com/boltchat/client/cli/cmd" 25 | cliIdentity "github.com/boltchat/client/cli/identity" 26 | "github.com/boltchat/client/config" 27 | "github.com/boltchat/client/errs" 28 | "github.com/boltchat/client/identity" 29 | "github.com/boltchat/client/tui" 30 | "github.com/boltchat/lib/client" 31 | "github.com/boltchat/protocol/events" 32 | "github.com/fatih/color" 33 | ) 34 | 35 | var ConnectCommand = &cmd.Command{ 36 | Name: "connect", 37 | Desc: "Connects to a Bolt instance.", 38 | Usage: " [identity]", 39 | Handler: connectHandler, 40 | } 41 | 42 | type Args struct { 43 | Hostname string 44 | Port int 45 | Identity string 46 | } 47 | 48 | // TODO: validation functions on commands 49 | func parseArgs(args []string) (*Args, error) { 50 | // Set identity to 'default' by default 51 | identity := config.DefaultIdentity 52 | 53 | if len(args) < 1 { 54 | return nil, errors.New("no host given") 55 | } 56 | 57 | if len(args) > 1 { 58 | identity = args[1] 59 | } 60 | 61 | splitHost := strings.Split(args[0], ":") 62 | hostname := splitHost[0] 63 | 64 | // The default port 65 | port := 3300 66 | 67 | // Custom port number is specified 68 | if len(splitHost) == 2 { 69 | parsedPort, parseErr := strconv.ParseInt(splitHost[1], 10, 32) 70 | 71 | if parseErr != nil { 72 | return nil, errors.New("invalid host") 73 | } 74 | 75 | port = int(parsedPort) 76 | } 77 | 78 | return &Args{ 79 | Hostname: hostname, 80 | Port: port, 81 | Identity: identity, 82 | }, nil 83 | } 84 | 85 | func connectHandler(args []string) error { 86 | connectArgs, parseErr := parseArgs(args) 87 | if parseErr != nil { 88 | return parseErr 89 | } 90 | 91 | // Attempt to read the identity 92 | idEntry, identityErr := config.GetIdentityEntry(connectArgs.Identity) 93 | var id *identity.Identity 94 | 95 | // TODO: refactor 96 | if identityErr == config.ErrNoSuchIdentity { 97 | if !cliIdentity.AskCreate(connectArgs.Identity) { 98 | os.Exit(1) 99 | } 100 | 101 | var createErr error 102 | id, createErr = cliIdentity.CreateIdentity(connectArgs.Identity) 103 | 104 | if createErr != nil { 105 | errs.Identity(createErr) 106 | } 107 | } else if identityErr != nil { 108 | errs.Identity(identityErr) 109 | } else { 110 | var loadErr error 111 | id, loadErr = identity.LoadIdentity(idEntry) 112 | 113 | if loadErr != nil { 114 | errs.Identity(loadErr) 115 | } 116 | } 117 | 118 | if id.Nickname == "" { 119 | errs.General( 120 | fmt.Sprintf( 121 | "It looks like you haven't set your nickname "+ 122 | "yet.\nPlease do so by editing the %s field in %s.", 123 | color.HiYellowString("nickname"), 124 | config.IdentityFile.GetLocation(), 125 | ), 126 | ) 127 | } 128 | 129 | c := client.NewClient(client.Options{ 130 | Hostname: connectArgs.Hostname, 131 | Port: connectArgs.Port, 132 | Identity: id, 133 | }) 134 | 135 | err := c.Connect() 136 | 137 | if err != nil { 138 | errs.Connect(err) 139 | } 140 | 141 | evts := make(chan *events.Event) 142 | 143 | serverClosed := make(chan bool) 144 | go c.ReadEvents(evts, serverClosed) 145 | go tui.Display(c, evts) 146 | 147 | // Quit when the server closes 148 | <-serverClosed 149 | tui.Quit() 150 | fmt.Println("The server closed.") 151 | os.Exit(0) 152 | 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /client/cli/cmd/help/help.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package help 16 | 17 | import ( 18 | "github.com/boltchat/client/cli/cmd" 19 | ) 20 | 21 | var HelpCommand = &cmd.Command{ 22 | Name: "help", 23 | Desc: "Displays this page.", 24 | Handler: nil, // Will be assigned by ParseCommand() 25 | } 26 | -------------------------------------------------------------------------------- /client/cli/cmd/identity/create.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package identity 16 | 17 | import ( 18 | "github.com/boltchat/client/cli/cmd" 19 | "github.com/boltchat/client/cli/identity" 20 | ) 21 | 22 | var CreateCommand = &cmd.Command{ 23 | Name: "create", 24 | Desc: "Creates a new Identity.", 25 | Handler: createHandler, 26 | } 27 | 28 | func createHandler(args []string) error { 29 | identity.CreateIdentity("") 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /client/cli/cmd/identity/identity.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package identity 16 | 17 | import "github.com/boltchat/client/cli/cmd" 18 | 19 | var IdentityCommand = &cmd.Command{ 20 | Name: "identity", 21 | Desc: "Identity subcommands.", 22 | Subcommands: []*cmd.Command{ 23 | CreateCommand, 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /client/cli/cmd/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/boltchat/client" 21 | "github.com/boltchat/client/cli/cmd" 22 | "github.com/boltchat/protocol" 23 | "github.com/boltchat/util/version" 24 | ) 25 | 26 | var VersionCommand = &cmd.Command{ 27 | Name: "version", 28 | Desc: "Displays version information.", 29 | Handler: versionHandler, 30 | } 31 | 32 | func versionHandler(args []string) error { 33 | fmt.Println(version.FormatVersion([]*version.Version{ 34 | client.Version, 35 | protocol.Version, 36 | })) 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /client/cli/identity/create.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package identity 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/boltchat/client/config" 22 | "github.com/boltchat/client/identity" 23 | "github.com/fatih/color" 24 | ) 25 | 26 | // CreateIdentity creates a new Identity. 27 | func CreateIdentity(identityID string) (*identity.Identity, error) { 28 | nickname := "" 29 | 30 | for strings.TrimSpace(identityID) == "" { 31 | fmt.Printf( 32 | color.HiCyanString("The Identity ID is used for referencing this Identity later on. An example would be %s or %s.\n"), 33 | color.HiYellowString("my_alt"), 34 | color.HiYellowString("very_very_secret"), 35 | ) 36 | fmt.Printf("Identity ID: ") 37 | fmt.Scanln(&identityID) 38 | } 39 | 40 | for strings.TrimSpace(nickname) == "" { 41 | fmt.Printf("Nickname: ") 42 | fmt.Scanln(&nickname) 43 | } 44 | 45 | return identity.CreateIdentity(&config.Identity{ 46 | Nickname: nickname, 47 | }, identityID) 48 | } 49 | 50 | // AskCreate will prompt the user if they'd like to create 51 | // the missing identity. 52 | func AskCreate(identityID string) bool { 53 | fmt.Printf( 54 | "Identity '%s' does not exist.\nWould you like to create it now? [Y/n] ", 55 | identityID, 56 | ) 57 | 58 | answer := "" 59 | fmt.Scanln(&answer) 60 | 61 | return strings.ToLower(answer) == "y" || answer == "" 62 | } 63 | -------------------------------------------------------------------------------- /client/config/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "github.com/boltchat/client/errs" 19 | "gopkg.in/yaml.v2" 20 | ) 21 | 22 | type Prompt struct { 23 | HOffset int `yaml:"hOffset"` 24 | } 25 | 26 | type StatusLine struct { 27 | Height int `yaml:"height"` 28 | } 29 | 30 | type Config struct { 31 | Prompt Prompt `yaml:"prompt"` 32 | StatusLine StatusLine `yaml:"statusLine"` 33 | } 34 | 35 | var config Config 36 | 37 | var ConfigFile = &File{ 38 | Filename: "config.yml", 39 | Default: *GetDefaultConfig(), 40 | } 41 | 42 | func parseConfig(raw []byte) (*Config, error) { 43 | config := &Config{} 44 | err := yaml.Unmarshal(raw, config) 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return config, nil 51 | } 52 | 53 | func LoadConfig() { 54 | configRaw, readErr := ConfigFile.Read() 55 | if readErr != nil { 56 | errs.Emerg(readErr) 57 | } 58 | 59 | conf, parseErr := parseConfig(configRaw) 60 | if parseErr != nil { 61 | errs.Emerg(parseErr) 62 | } 63 | 64 | config = *conf 65 | } 66 | 67 | func GetConfig() *Config { 68 | return &config 69 | } 70 | -------------------------------------------------------------------------------- /client/config/default.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | func GetDefaultConfig() *Config { 18 | return &Config{ 19 | Prompt: Prompt{ 20 | HOffset: 1, 21 | }, 22 | StatusLine: StatusLine{ 23 | Height: 1, 24 | }, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /client/config/file.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "io/ioutil" 19 | "os" 20 | "path" 21 | 22 | "github.com/boltchat/client/errs" 23 | "gopkg.in/yaml.v2" 24 | ) 25 | 26 | // File is used to describe a YAML config file. 27 | type File struct { 28 | Filename string 29 | Default interface{} 30 | } 31 | 32 | // GetConfigRoot returns the base folder where all 33 | // config files reside. 34 | func GetConfigRoot() string { 35 | root, err := os.UserConfigDir() 36 | if err != nil { 37 | errs.Emerg(err) 38 | } 39 | 40 | return path.Join(root, "boltchat") 41 | } 42 | 43 | func (f *File) GetLocation() string { 44 | return path.Join(GetConfigRoot(), f.Filename) 45 | } 46 | 47 | func (f *File) Read() ([]byte, error) { 48 | configLocation := f.GetLocation() 49 | configRaw, err := ioutil.ReadFile(configLocation) 50 | 51 | if err != nil && !os.IsNotExist(err) { 52 | return nil, err 53 | } 54 | 55 | if len(configRaw) == 0 { 56 | writeRes, writeErr := f.Write(f.Default) 57 | if writeErr != nil { 58 | return nil, writeErr 59 | } 60 | 61 | configRaw = writeRes 62 | } 63 | 64 | return configRaw, nil 65 | } 66 | 67 | func (f *File) Write(data interface{}) ([]byte, error) { 68 | configRoot := GetConfigRoot() 69 | configLocation := f.GetLocation() 70 | conf, marshalErr := yaml.Marshal(data) 71 | 72 | if marshalErr != nil { 73 | return nil, marshalErr 74 | } 75 | 76 | stat, statErr := os.Stat(configRoot) 77 | if statErr != nil || !stat.IsDir() { 78 | os.MkdirAll(configRoot, 0755) 79 | } 80 | 81 | writeErr := ioutil.WriteFile(configLocation, conf, 0644) 82 | if writeErr != nil { 83 | return nil, writeErr 84 | } 85 | 86 | return conf, nil 87 | } 88 | -------------------------------------------------------------------------------- /client/config/identity.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package config 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/boltchat/client/errs" 21 | "gopkg.in/yaml.v2" 22 | ) 23 | 24 | var ErrNoSuchIdentity = errors.New("identity not found") 25 | 26 | type Identity struct { 27 | ID string `yaml:"-"` 28 | Nickname string `yaml:"nickname"` 29 | } 30 | 31 | type IdentityList map[string]Identity 32 | 33 | var identityList IdentityList 34 | 35 | var IdentityFile = &File{ 36 | Filename: "identity.yml", 37 | Default: IdentityList{}, 38 | } 39 | 40 | const DefaultIdentity string = "default" 41 | 42 | func parseIdentityList(raw []byte) (*IdentityList, error) { 43 | identityList := &IdentityList{} 44 | err := yaml.Unmarshal(raw, identityList) 45 | 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return identityList, nil 51 | } 52 | 53 | func LoadIdentityList() { 54 | identityRaw, readErr := IdentityFile.Read() 55 | if readErr != nil { 56 | errs.Emerg(readErr) 57 | } 58 | 59 | identity, parseErr := parseIdentityList(identityRaw) 60 | if parseErr != nil { 61 | errs.Emerg(readErr) 62 | } 63 | 64 | identityList = *identity 65 | } 66 | 67 | func GetIdentityList() *IdentityList { 68 | return &identityList 69 | } 70 | 71 | func GetIdentityEntry(id string) (*Identity, error) { 72 | if identity, ok := identityList[id]; ok { 73 | identity.ID = id 74 | return &identity, nil 75 | } 76 | 77 | return nil, ErrNoSuchIdentity 78 | } 79 | -------------------------------------------------------------------------------- /client/errs/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package errs 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | 21 | "github.com/fatih/color" 22 | ) 23 | 24 | type SyntaxError struct { 25 | Error error 26 | Desc string 27 | } 28 | 29 | /* 30 | Emerg displays a message to the user that something has 31 | gone wrong internally, and immediately stops program 32 | execution afterwards. 33 | */ 34 | func Emerg(err error) { 35 | fmt.Printf(color.HiRedString( 36 | "An unexpected error has occurred.\nPlease consider creating " + 37 | "an issue at " + 38 | "if this is repetitive behaviour.\n", 39 | )) 40 | 41 | // Immediately stop execution 42 | panic(err) 43 | } 44 | 45 | /* 46 | Syntax tells the user that they've made a syntax error 47 | and cleanly exits the program afterwards. 48 | */ 49 | func Syntax(err SyntaxError) { 50 | fmt.Printf("Syntax error: %s\n", err.Desc) 51 | os.Exit(1) 52 | } 53 | 54 | /* 55 | Connect informs the user about an error that has occured while 56 | attempting to connect to the server. 57 | */ 58 | func Connect(err error) { 59 | fmt.Printf("Connection error: %s\n", err.Error()) 60 | os.Exit(1) 61 | } 62 | 63 | /* 64 | General is used for general errors. 65 | */ 66 | func General(err string) { 67 | fmt.Printf("General error: %s\n", err) 68 | os.Exit(1) 69 | } 70 | 71 | /* 72 | Identity is used for Identity-related errors. 73 | */ 74 | func Identity(err error) { 75 | fmt.Printf("Identity error: %s\n", err) 76 | os.Exit(1) 77 | } 78 | -------------------------------------------------------------------------------- /client/format/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/boltchat/protocol/errs" 21 | "github.com/boltchat/protocol/events" 22 | "github.com/mitchellh/mapstructure" 23 | 24 | "github.com/fatih/color" 25 | ) 26 | 27 | var errorMap = map[string]string{ 28 | errs.InvalidEvent: "This event type does not exist.", 29 | errs.InvalidFormat: "The format of your request could not be parsed.", 30 | errs.TooManyMessages: "You're sending too many messages. Please slow down.", 31 | errs.Unidentified: "You need to identify yourself before you can interact with this server.", 32 | errs.SigVerifyFailed: "Signature verification failed. You might need to recreate your Identity.", 33 | errs.CommandNotFound: "Command not found.", 34 | } 35 | 36 | func FormatError(e *events.Event) string { 37 | errData := events.ErrorData{} 38 | mapstructure.Decode(e.Data, &errData) 39 | 40 | err := errData.Error 41 | 42 | // A formatter exists for this error 43 | if format, ok := errorMap[errData.Error]; ok { 44 | err = format 45 | } 46 | 47 | return color.HiRedString( 48 | fmt.Sprintf("[!] %s", err), 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /client/format/format.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/boltchat/protocol/events" 21 | ) 22 | 23 | type formatHandler = func(e *events.Event) string 24 | 25 | var formatMap = map[events.Type]formatHandler{ 26 | events.MotdType: FormatMotd, 27 | events.MessageType: FormatMessage, 28 | events.ErrorType: FormatError, 29 | events.JoinType: FormatJoin, 30 | events.LeaveType: FormatLeave, 31 | events.NoticeType: FormatNotice, 32 | } 33 | 34 | // Format formats an event in a human-readable format. 35 | func Format(evt *events.Event) string { 36 | if formatFunc, ok := formatMap[evt.Meta.Type]; ok { 37 | return formatFunc(evt) 38 | } 39 | return fmt.Sprintf("unable to format event: %v", evt.Meta.Type) 40 | } 41 | -------------------------------------------------------------------------------- /client/format/join.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/boltchat/protocol/events" 21 | "github.com/fatih/color" 22 | "github.com/gdamore/tcell/v2" 23 | "github.com/mitchellh/mapstructure" 24 | ) 25 | 26 | func FormatJoin(e *events.Event) string { 27 | joinData := events.JoinData{} 28 | mapstructure.Decode(e.Data, &joinData) 29 | 30 | return color.HiMagentaString( 31 | fmt.Sprintf("%s %s joined the room.", string(tcell.RuneDiamond), joinData.User.Nickname), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /client/format/leave.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/boltchat/protocol/events" 21 | "github.com/fatih/color" 22 | "github.com/gdamore/tcell/v2" 23 | "github.com/mitchellh/mapstructure" 24 | ) 25 | 26 | func FormatLeave(e *events.Event) string { 27 | leaveData := events.LeaveData{} 28 | mapstructure.Decode(e.Data, &leaveData) 29 | 30 | return color.HiMagentaString( 31 | fmt.Sprintf("%s %s left the room.", string(tcell.RuneDiamond), leaveData.User.Nickname), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /client/format/message.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | 21 | "github.com/boltchat/client/markdown" 22 | "github.com/boltchat/protocol/events" 23 | "github.com/fatih/color" 24 | "github.com/mitchellh/mapstructure" 25 | ) 26 | 27 | func FormatMessage(e *events.Event) string { 28 | msgData := events.MessageData{} 29 | mapstructure.Decode(e.Data, &msgData) 30 | 31 | fprint := msgData.Message.Fingerprint 32 | 33 | // Use the last four characters of the fingerprint 34 | // for the user tag. 35 | tag := fprint[len(fprint)-4:] 36 | 37 | return fmt.Sprintf( 38 | "<%s#%s> %s", 39 | msgData.Message.User.Nickname, 40 | color.HiYellowString(strings.ToUpper(tag)), 41 | markdown.ReplaceAll(msgData.Message.Content), 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /client/format/motd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "github.com/boltchat/protocol/events" 19 | "github.com/mitchellh/mapstructure" 20 | 21 | "github.com/fatih/color" 22 | ) 23 | 24 | func FormatMotd(e *events.Event) string { 25 | motdData := events.MotdData{} 26 | mapstructure.Decode(e.Data, &motdData) 27 | 28 | return color.HiCyanString(motdData.Motd) 29 | } 30 | -------------------------------------------------------------------------------- /client/format/notice.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package format 16 | 17 | import ( 18 | "fmt" 19 | 20 | "github.com/boltchat/protocol/events" 21 | "github.com/mitchellh/mapstructure" 22 | 23 | "github.com/fatih/color" 24 | ) 25 | 26 | func FormatNotice(e *events.Event) string { 27 | noticeData := events.NoticeData{} 28 | mapstructure.Decode(e.Data, ¬iceData) 29 | 30 | return color.HiCyanString( 31 | fmt.Sprintf("[i] %s", noticeData.Message), 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /client/identity/create.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package identity 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/boltchat/client/config" 21 | "github.com/boltchat/lib/pgp" 22 | ) 23 | 24 | // CreateIdentity creates a new Identity. 25 | func CreateIdentity(identity *config.Identity, identityID string) (*Identity, error) { 26 | identityList := *config.GetIdentityList() 27 | identityList[identityID] = *identity 28 | 29 | // Create new PGP entity 30 | entity, createErr := pgp.CreatePGPEntity(identity.Nickname) 31 | 32 | if createErr != nil { 33 | return nil, createErr 34 | } 35 | 36 | if _, statErr := os.Stat(GetEntityRoot()); os.IsNotExist(statErr) { 37 | os.Mkdir(GetEntityRoot(), 0700) 38 | } else if statErr != nil { 39 | return nil, statErr 40 | } 41 | 42 | // Write the PGP entity to disk 43 | writePGPErr := pgp.WritePGPEntity(GetEntityLocation(identityID), entity) 44 | if writePGPErr != nil { 45 | return nil, writePGPErr 46 | } 47 | 48 | // Write the config changes to disk 49 | _, writeErr := config.IdentityFile.Write(identityList) 50 | if writeErr != nil { 51 | return nil, writeErr 52 | } 53 | 54 | return &Identity{ 55 | Nickname: identity.Nickname, 56 | Entity: entity, 57 | }, nil 58 | } 59 | -------------------------------------------------------------------------------- /client/identity/entity.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package identity 16 | 17 | import ( 18 | "fmt" 19 | "path" 20 | 21 | "github.com/boltchat/client/config" 22 | ) 23 | 24 | func GetEntityRoot() string { 25 | return path.Join( 26 | config.GetConfigRoot(), 27 | "entities", 28 | ) 29 | } 30 | 31 | func GetEntityLocation(identityID string) string { 32 | return path.Join( 33 | GetEntityRoot(), 34 | fmt.Sprintf("%s.pgp", identityID), 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /client/identity/identity.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package identity 16 | 17 | import ( 18 | "golang.org/x/crypto/openpgp" 19 | ) 20 | 21 | type Identity struct { 22 | Nickname string 23 | Entity *openpgp.Entity 24 | } 25 | -------------------------------------------------------------------------------- /client/identity/load.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package identity 16 | 17 | import ( 18 | "github.com/boltchat/client/config" 19 | "github.com/boltchat/lib/pgp" 20 | ) 21 | 22 | func LoadIdentity(identity *config.Identity) (*Identity, error) { 23 | entityPath := GetEntityLocation(identity.ID) 24 | entity, err := pgp.LoadPGPEntity(entityPath) 25 | 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | return &Identity{ 31 | Nickname: identity.Nickname, 32 | Entity: entity, 33 | }, nil 34 | } 35 | -------------------------------------------------------------------------------- /client/markdown/bold.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package markdown 16 | 17 | import ( 18 | "regexp" 19 | "strings" 20 | 21 | "github.com/fatih/color" 22 | ) 23 | 24 | const BoldRegex string = "(\\*+)(\\s*\\b)([^\\*]*)(\\b\\s*)(\\*+)" 25 | 26 | func BoldReplacer(r *regexp.Regexp) func(s string) string { 27 | return func(s string) string { 28 | s = strings.ReplaceAll(s, "*", "") 29 | return color.New(color.Bold).Sprintf(s) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /client/markdown/markdown.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package markdown 16 | 17 | import ( 18 | "regexp" 19 | ) 20 | 21 | type ReplacerFunc func(r *regexp.Regexp) func(s string) string 22 | 23 | var replacers = map[string]ReplacerFunc{ 24 | BoldRegex: BoldReplacer, 25 | } 26 | 27 | func ReplaceAll(src string) (out string) { 28 | for regex, replacer := range replacers { 29 | r := regexp.MustCompile(regex) 30 | out = r.ReplaceAllStringFunc(src, replacer(r)) 31 | } 32 | 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /client/tui/chatbox/chatbox.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package chatbox 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | "time" 21 | 22 | "github.com/boltchat/client/config" 23 | "github.com/boltchat/client/format" 24 | "github.com/boltchat/client/tui/util" 25 | "github.com/boltchat/protocol/events" 26 | "github.com/fatih/color" 27 | 28 | "github.com/gdamore/tcell/v2" 29 | ) 30 | 31 | func printEvent(s tcell.Screen, w int, y int, evt *events.Event) int { 32 | // Convert event timestamp to `time.Time` 33 | timestamp := time.Unix(evt.Meta.CreatedAt, 0) 34 | 35 | // Format the timestamp string 36 | timestampStr := strings.Join([]string{ 37 | color.HiBlackString("["), 38 | timestamp.Format(time.Stamp), 39 | color.HiBlackString("]"), 40 | }, "") 41 | 42 | /* 43 | Calculate prefix length 44 | 45 | TODO: refactor. This is a temporary workaround 46 | because I have not yet found an optimal way of 47 | extracting control characters/ANSI colors from 48 | `timestampStr` and counting the length of that 49 | instead. 50 | */ 51 | prefixLen := len(fmt.Sprintf( 52 | "[%s] ", 53 | timestamp.Format(time.Stamp), 54 | )) 55 | 56 | evtContent := format.Format(evt) 57 | evtPrefix := timestampStr + " " 58 | evtStr := evtPrefix + evtContent 59 | 60 | /* 61 | Preallocate one chunk because we're certain 62 | that there will always be at least one 63 | chunk in the `chunks` array. 64 | */ 65 | chunks := make([]string, 0, 1) 66 | 67 | // Split the event into an array of chunks 68 | for _, line := range strings.Split(evtStr, "\n") { 69 | chunks = append(chunks, util.SplitChunks(line, w-prefixLen)...) 70 | } 71 | 72 | for offset, line := range chunks { 73 | if offset > 0 { 74 | line = strings.Repeat(" ", prefixLen) + line 75 | } 76 | 77 | util.PrintLine(s, 0, y+offset, line) 78 | } 79 | 80 | return len(chunks) - 1 81 | } 82 | 83 | func getBufferDimensions(s tcell.Screen) (h, yOffset int) { 84 | // Get heights & offsets 85 | _, sHeight := s.Size() 86 | statusHeight := config.GetConfig().StatusLine.Height 87 | promptHOffset := config.GetConfig().Prompt.HOffset 88 | 89 | h = sHeight - promptHOffset - statusHeight 90 | yOffset = statusHeight 91 | return 92 | } 93 | 94 | func clearBuffer(s tcell.Screen) { 95 | hBuff, yOffset := getBufferDimensions(s) 96 | w, _ := s.Size() 97 | 98 | // Clear the buffer 99 | for y := 0; y < hBuff; y++ { 100 | util.ClearLine(s, y+yOffset, w) 101 | } 102 | } 103 | 104 | func DisplayChatbox( 105 | s tcell.Screen, 106 | evtChannel chan *events.Event, 107 | clear chan bool, 108 | ) { 109 | /* 110 | Preallocate a size of 50 for both the 111 | events slice and the buffer slice. 112 | */ 113 | evts := make([]*events.Event, 0, 50) 114 | buff := make([]*events.Event, 0, 50) 115 | 116 | go func() { 117 | for evt := range evtChannel { 118 | w, _ := s.Size() 119 | hBuff, yOffset := getBufferDimensions(s) 120 | 121 | // Append event to the events slice 122 | evts = append(evts, evt) 123 | 124 | if len(buff) < hBuff { 125 | // Append event to buffer 126 | buff = append(buff, evt) 127 | } else { 128 | // Remove first event from buffer and append 129 | buff = append(buff[1:], evt) 130 | } 131 | 132 | // Clear the buffer 133 | clearBuffer(s) 134 | 135 | // Append all events to the chatbox buffer 136 | for y, event := range buff { 137 | yOffset += printEvent(s, w, y+yOffset, event) 138 | } 139 | 140 | /* 141 | FIXME: This is for the time being. See issue #2 142 | for more information. 143 | */ 144 | s.Sync() 145 | } 146 | }() 147 | 148 | go func() { 149 | for c := range clear { 150 | if c { 151 | buff = make([]*events.Event, 0, 50) 152 | clearBuffer(s) 153 | s.Sync() 154 | } 155 | } 156 | }() 157 | } 158 | -------------------------------------------------------------------------------- /client/tui/prompt/mode_cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package prompt 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/boltchat/lib/client" 21 | "github.com/boltchat/protocol/events" 22 | "github.com/gdamore/tcell/v2" 23 | ) 24 | 25 | func handleCommandMode(s tcell.Screen, c *client.Client, evt tcell.Event) { 26 | key, ok := evt.(*tcell.EventKey) 27 | if !ok { 28 | return 29 | } 30 | 31 | if (key.Key() == tcell.KeyBackspace || key.Key() == tcell.KeyBackspace2) && 32 | len(input) == 1 && 33 | input[0] == '/' { 34 | // Return to message mode 35 | mode = MessageMode 36 | return 37 | } 38 | 39 | if key.Key() == tcell.KeyEnter && len(input) > 1 { 40 | sendCommand(c) 41 | mode = MessageMode 42 | clearInput() 43 | } 44 | } 45 | 46 | func sendCommand(c *client.Client) { 47 | cmdSplit := strings.Split(string(input[1:]), " ") 48 | cmd := cmdSplit[0] 49 | args := cmdSplit[1:] 50 | 51 | c.SendCommand(&events.CommandData{ 52 | Command: cmd, 53 | Args: args, 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /client/tui/prompt/mode_msg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package prompt 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/boltchat/client/errs" 21 | "github.com/boltchat/lib/client" 22 | "github.com/boltchat/protocol" 23 | "github.com/gdamore/tcell/v2" 24 | ) 25 | 26 | func handleMessageMode(s tcell.Screen, c *client.Client, evt tcell.Event) { 27 | key, ok := evt.(*tcell.EventKey) 28 | if !ok { 29 | return 30 | } 31 | 32 | if key.Key() == tcell.KeyEnter { 33 | sendMessage(c) 34 | } 35 | } 36 | 37 | func sendMessage(c *client.Client) { 38 | body := strings.TrimSpace(string(input)) 39 | 40 | if len(body) < 1 { 41 | return 42 | } 43 | 44 | msg := protocol.Message{ 45 | Content: body, 46 | User: &protocol.User{ 47 | Nickname: c.Identity.Nickname, // TODO 48 | }, 49 | } 50 | 51 | signErr := c.SignMessage(&msg) 52 | if signErr != nil { 53 | errs.Emerg(signErr) 54 | } 55 | 56 | sendErr := c.SendMessage(&msg) 57 | if sendErr != nil { 58 | errs.Emerg(sendErr) 59 | } 60 | 61 | // Clear input 62 | clearInput() 63 | } 64 | -------------------------------------------------------------------------------- /client/tui/prompt/modes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package prompt 16 | 17 | import ( 18 | "github.com/boltchat/lib/client" 19 | "github.com/gdamore/tcell/v2" 20 | ) 21 | 22 | type Mode int 23 | 24 | type ModeHandler func(s tcell.Screen, c *client.Client, evt tcell.Event) 25 | 26 | const ( 27 | MessageMode Mode = iota 28 | CommandMode Mode = iota 29 | ) 30 | 31 | var modeHandlers = map[Mode]ModeHandler{ 32 | MessageMode: handleMessageMode, 33 | CommandMode: handleCommandMode, 34 | } 35 | 36 | var modeStrs = map[Mode]string{ 37 | MessageMode: "Msg", 38 | CommandMode: "Cmd", 39 | } 40 | -------------------------------------------------------------------------------- /client/tui/prompt/prompt.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package prompt 16 | 17 | import ( 18 | "os" 19 | "unicode" 20 | 21 | "github.com/boltchat/client/config" 22 | "github.com/boltchat/client/tui/util" 23 | "github.com/boltchat/lib/client" 24 | "github.com/gdamore/tcell/v2" 25 | ) 26 | 27 | // FIXME: thread-unsafe 28 | var input []rune 29 | 30 | // FIXME: thread-unsafe 31 | var mode Mode 32 | 33 | func drawPrompt(s tcell.Screen) { 34 | w, h := s.Size() 35 | arrowStyle := tcell.StyleDefault.Foreground(tcell.ColorYellow).Bold(true) 36 | 37 | y := h - config.GetConfig().Prompt.HOffset 38 | 39 | modeStr := modeStrs[mode] 40 | modeLen := len(modeStr) 41 | inputLen := len(input) 42 | 43 | arrowXPos := modeLen + 1 44 | inputXPos := inputLen + arrowXPos 45 | 46 | // Clear prompt line 47 | util.ClearLine(s, y, w) 48 | 49 | // Print prompt mode 50 | for i := 0; i < modeLen; i++ { 51 | s.SetContent(i, y, rune(modeStr[i]), nil, tcell.StyleDefault.Bold(true)) 52 | } 53 | 54 | // Print prompt arrow 55 | s.SetContent(arrowXPos, y, '>', nil, arrowStyle) 56 | 57 | // Print user input 58 | for i := 0; i < inputLen; i++ { 59 | s.SetContent(i+arrowXPos+2, y, input[i], nil, tcell.StyleDefault) 60 | } 61 | 62 | // Draw cursor after input 63 | s.ShowCursor(inputXPos+2, y) 64 | 65 | s.Show() 66 | } 67 | 68 | func handleEvents(s tcell.Screen, c *client.Client, termEvts chan tcell.Event, clear chan bool) { 69 | for termEvt := range termEvts { 70 | // Execute mode handlers 71 | modeHandlers[mode](s, c, termEvt) 72 | 73 | switch termEvt.(type) { 74 | case *tcell.EventKey: 75 | evt := termEvt.(*tcell.EventKey) 76 | 77 | if evt.Key() == tcell.KeyEscape || 78 | evt.Key() == tcell.KeyCtrlC || 79 | evt.Key() == tcell.KeyCtrlD { 80 | // Exit TUI 81 | s.Fini() 82 | os.Exit(0) 83 | return 84 | } else if evt.Key() == tcell.KeyCtrlL { 85 | go func() { clear <- true }() 86 | } else if evt.Key() == tcell.KeyBackspace || 87 | evt.Key() == tcell.KeyBackspace2 { 88 | if len(input) > 0 { 89 | input = input[:len(input)-1] 90 | } 91 | } else if evt.Key() == tcell.KeyCtrlU { 92 | clearInput() 93 | } else if evt.Key() == tcell.KeyUp || 94 | evt.Key() == tcell.KeyDown || 95 | evt.Key() == tcell.KeyLeft || 96 | evt.Key() == tcell.KeyRight || 97 | evt.Key() == tcell.KeyHome || 98 | evt.Key() == tcell.KeyEnd { 99 | // TODO: add logic 100 | break 101 | } else if evt.Rune() == '/' && len(input) == 0 { 102 | input = append(input, evt.Rune()) 103 | mode = CommandMode 104 | } else { 105 | // Append the character if it's visible 106 | if unicode.IsGraphic(evt.Rune()) { 107 | input = append(input, evt.Rune()) 108 | } 109 | } 110 | 111 | drawPrompt(s) 112 | } 113 | } 114 | } 115 | 116 | func DisplayPrompt(s tcell.Screen, c *client.Client, termEvts chan tcell.Event, clear chan bool) { 117 | // Initialize vars 118 | clearInput() 119 | mode = MessageMode 120 | 121 | // Draw initial (empty) prompt 122 | drawPrompt(s) 123 | 124 | // Handle terminal events 125 | go handleEvents(s, c, termEvts, clear) 126 | } 127 | -------------------------------------------------------------------------------- /client/tui/prompt/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package prompt 16 | 17 | func clearInput() { 18 | input = make([]rune, 0, 50) 19 | } 20 | -------------------------------------------------------------------------------- /client/tui/statusline/statusline.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package statusline 16 | 17 | import ( 18 | "github.com/boltchat/client" 19 | "github.com/boltchat/client/tui/util" 20 | "github.com/boltchat/protocol/events" 21 | "github.com/fatih/color" 22 | "github.com/gdamore/tcell/v2" 23 | ) 24 | 25 | func DisplayStatusLine(s tcell.Screen, evtsChan chan *events.Event) { 26 | util.PrintLine(s, 0, 0, color.CyanString("Bolt Client v%s", client.Version.VersionString)) 27 | s.Show() 28 | } 29 | -------------------------------------------------------------------------------- /client/tui/tui.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package tui 16 | 17 | import ( 18 | "github.com/boltchat/client/errs" 19 | "github.com/boltchat/client/tui/chatbox" 20 | "github.com/boltchat/client/tui/prompt" 21 | "github.com/boltchat/client/tui/statusline" 22 | "github.com/boltchat/lib/client" 23 | "github.com/boltchat/protocol/events" 24 | 25 | "github.com/gdamore/tcell/v2" 26 | "github.com/gdamore/tcell/v2/encoding" 27 | ) 28 | 29 | var screen tcell.Screen 30 | 31 | /* 32 | Display displays the TUI. 33 | */ 34 | func Display(c *client.Client, evts chan *events.Event) { 35 | // Register all encodings 36 | encoding.Register() 37 | 38 | // Channels 39 | clear := make(chan bool) 40 | termEvts := make(chan tcell.Event) 41 | 42 | // Create a screen 43 | s, err := tcell.NewScreen() 44 | screen = s 45 | 46 | if err != nil { 47 | errs.Emerg(err) 48 | } 49 | 50 | // Initialize the screen 51 | if err := s.Init(); err != nil { 52 | errs.Emerg(err) 53 | } 54 | 55 | // Set default style 56 | s.SetStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite)) 57 | 58 | // Display prompt and chatbox 59 | go prompt.DisplayPrompt(s, c, termEvts, clear) 60 | go chatbox.DisplayChatbox(s, evts, clear) 61 | go statusline.DisplayStatusLine(s, evts) 62 | 63 | // Poll terminal events 64 | go func() { 65 | for { 66 | termEvts <- s.PollEvent() 67 | } 68 | }() 69 | } 70 | 71 | // Quit quits the TUI. 72 | func Quit() { 73 | screen.Fini() 74 | } 75 | -------------------------------------------------------------------------------- /client/tui/util/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "github.com/gdamore/tcell/v2" 19 | ) 20 | 21 | func SplitChunks(str string, n int) []string { 22 | /* 23 | Return one chunk containing the entire string 24 | if the string does not exceed `n`. 25 | */ 26 | if len(str) < n { 27 | return []string{str} 28 | } 29 | 30 | chunks := make([]string, 0) 31 | 32 | for i := 0; i < len(str); i++ { 33 | if i%n == 0 { 34 | end := i + n 35 | 36 | if end > len(str) { 37 | end = len(str) 38 | } 39 | 40 | chunks = append(chunks, str[i:end]) 41 | } 42 | } 43 | 44 | return chunks 45 | } 46 | 47 | func PrintLine(s tcell.Screen, x int, y int, str string) { 48 | /* 49 | I do not like this workaround at all, but at this 50 | point, I've given up on trying to find a better 51 | solution. Feel free to create a Pull Request if 52 | you're able to improve this. 53 | */ 54 | chars := []rune("\b\b" + str) 55 | 56 | s.SetContent(x, y, ' ', chars[1:], tcell.StyleDefault) 57 | } 58 | 59 | func ClearLine(s tcell.Screen, y int, w int) { 60 | // Clear every cell to `w` 61 | for x := 0; x < w; x++ { 62 | s.SetContent(x, y, ' ', nil, tcell.StyleDefault) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /client/tui/util/util_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "testing" 19 | ) 20 | 21 | func TestSplitChunks(t *testing.T) { 22 | str := "Lorem ipsum dolor sit amet, consectetur adipiscing elit." 23 | 24 | got := SplitChunks(str, 14) 25 | want := []string{"Lorem ipsum do", "lor sit amet, ", "consectetur ad", "ipiscing elit."} 26 | 27 | for i, v := range want { 28 | if got[i] != v { 29 | t.Errorf("got %s want %s", got, want) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import "github.com/boltchat/util/version" 18 | 19 | var Version = &version.Version{ 20 | Type: version.ClientType, 21 | VersionString: "0.2.0-alpha", 22 | } 23 | -------------------------------------------------------------------------------- /cmd/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/boltchat/client/cli" 21 | "github.com/boltchat/client/config" 22 | ) 23 | 24 | func main() { 25 | // Load the config 26 | config.LoadConfig() 27 | config.LoadIdentityList() 28 | 29 | cmd, cmdErr := cli.ParseCommand(os.Args[1:]) 30 | if cmdErr != nil { 31 | cli.HandleCommandError(cmdErr) 32 | } 33 | 34 | execErr := cmd.Execute() 35 | if execErr != nil { 36 | cli.HandleCommandError(execErr) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /cmd/server/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "os" 19 | "os/signal" 20 | "syscall" 21 | "time" 22 | 23 | "github.com/boltchat/server" 24 | "github.com/boltchat/server/plugins" 25 | "github.com/boltchat/server/util" 26 | ) 27 | 28 | func main() { 29 | mgr := &plugins.PluginManager{} 30 | 31 | // Install plugins 32 | mgr.Install( 33 | plugins.RateLimiterPlugin{ 34 | Amount: 5, 35 | Time: time.Second, 36 | }, 37 | plugins.NicknameValidationPlugin{ 38 | MinChars: 2, 39 | MaxChars: 24, 40 | }, 41 | ) 42 | 43 | // Set the plugin manager 44 | plugins.SetManager(mgr) 45 | 46 | // Print ASCII banner 47 | util.PrintBanner() 48 | 49 | ipv4Bind := os.Getenv("BIND_IPV4") 50 | ipv6Bind := os.Getenv("BIND_IPV6") 51 | 52 | if ipv4Bind == "" { 53 | // Set default IPv4 bind to loopback address 54 | ipv4Bind = "127.0.0.1" 55 | } 56 | 57 | if ipv6Bind == "" { 58 | // Set default IPv6 bind to loopback address 59 | ipv6Bind = "::1" 60 | } 61 | 62 | listener := server.Listener{ 63 | Bind: []server.Bind{ 64 | {Address: ipv4Bind, Proto: "tcp4"}, 65 | {Address: ipv6Bind, Proto: "tcp6"}, 66 | }, 67 | Port: 3300, 68 | } 69 | 70 | err := listener.Listen() 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | // Exit on syscall 76 | sigs := make(chan os.Signal, 1) 77 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) 78 | <-sigs 79 | } 80 | -------------------------------------------------------------------------------- /conf/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | server: 5 | image: boltchat/server:latest 6 | ports: 7 | - 3300:3300 8 | environment: 9 | - BIND_IPV4=0.0.0.0 10 | - BIND_IPV6=0.0.0.0 11 | -------------------------------------------------------------------------------- /conf/linux/runit/boltchat/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | exec /usr/local/bin/boltchat-server 3 | -------------------------------------------------------------------------------- /conf/linux/systemd/boltchat.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Boltchat Server 3 | After=network.target 4 | StartLimitIntervalSec=0 5 | 6 | [Service] 7 | Type=simple 8 | Restart=always 9 | RestartSec=1 10 | ExecStart=/usr/local/bin/boltchat-server 11 | Environment=BIND_IPV4=0.0.0.0 12 | Environment=BIND_IPV6=::/0 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at `// TODO`. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | ## Client & server 4 | ### Binaries 5 | Please have a look at the [Releases](https://github.com/boltchat/bolt/releases) page for the most 6 | up-to-date client and server binaries. 7 | 8 | ### From source 9 | If you'd like to compile _Bolt_ from source, please follow the steps below: 10 | 11 | #### Prerequisites 12 | * Git 13 | * Go (v1.15.6) 14 | 15 | #### Cloning & installing 16 | ```bash 17 | git clone git@github.com:boltchat/bolt.git 18 | cd bolt 19 | go get github.com/magefile/mage 20 | go install github.com/magefile/mage 21 | ``` 22 | 23 | #### Building 24 | Run `mage` to see all available targets. 25 | 26 | ## Server 27 | ### Docker (preferred) 28 | If you'd like to run the server in a Docker container, you should use the most up-to-date 29 | image from [Docker Hub](https://hub.docker.com/r/boltchat/server). The following command 30 | should get you up and running within seconds: 31 | 32 | ```bash 33 | docker run -p 3300:3300 --tty boltchat/server:latest 34 | ``` 35 | 36 | ### Docker Compose 37 | Sample configuration can be found [here](../conf/docker/docker-compose.yml). 38 | 39 | ### Daemon 40 | You can also run the server as a daemon. Service files can be found below: 41 | * [`systemd` service](../conf/linux/systemd/boltchat.service) 42 | * [`runit` service](../conf/linux/runit) 43 | -------------------------------------------------------------------------------- /docs/protocol-spec.md: -------------------------------------------------------------------------------- 1 | # Protocol Specification 2 | > ⚠ Everything contained in this document is subject to change until the first stable release (v1.0.0). Parts denoted with a '⚠' symbol have a certainty of being changed in the near future. 3 | 4 | > **Legend:** 5 | > 6 | > `-->` client-to-server (send) 7 | > 8 | > `<--` server-to-client (receive) 9 | > 10 | > `<-->` client-to-server and server-to-client (send and receive) 11 | 12 | _Bolt_ exchanges information by sending JSON-marshalled data over a TCP connection. The exchanged information is often in the form of [Events](#events). 13 | 14 | ## Events 15 | Events represent any kind of activity happening in a server. They can be emitted by both the client and the server. Emitted events may not be limited to one connection only: some events are broadcasted by the server to all connected clients or to a selection of connected clients. 16 | 17 | The most basic form of an event looks like this: 18 | 19 | `<-->` 20 | ```json 21 | { 22 | "e": { 23 | "t": "msg", 24 | "c": 1611670138 25 | }, 26 | "d": { 27 | ... 28 | } 29 | } 30 | ``` 31 | 32 | | key | type | desc | 33 | |-----|---------------------------|----------------------| 34 | | `e` | [`EventMeta`](#eventmeta) | Event metadata | 35 | | `d` | [`EventData`](#eventdata) | Event data | 36 | 37 | The `...` indicates additional data that is accompanied with the event, depending on the type of event. 38 | 39 | ### `EventMeta` 40 | Basic metadata that is accompanied with each event. 41 | 42 | | key | type | desc | 43 | |-----|-------------|----------------------| 44 | | `t` | `string` | Type identifier | 45 | | `c` | `int64` | Creation date (Unix) | 46 | 47 | ### `EventData` 48 | See below for an overview of data types. 49 | 50 | ## Messages 51 | Messages represent chat messages. They can be sent by the client only. The server is responsible for delivering these messages to the intended recipients. 52 | 53 | A message looks like this: 54 | 55 | `-->` 56 | ```json 57 | { 58 | "body": "This is a message.", 59 | "sig": "-----BEGIN PGP SIGNATURE----- (...)", 60 | "user": { 61 | "nick": "keesvv" 62 | } 63 | } 64 | ``` 65 | > ⚠ `user` will soon be removed from message events emitted by clients. 66 | 67 | `<--` 68 | ```json 69 | { 70 | "body": "This is a message.", 71 | "fprint": "131e12c7087e576743cb6c26eaf3f4d4ee6305a9", 72 | "user": { 73 | "nick": "keesvv" 74 | } 75 | } 76 | ``` 77 | 78 | | key | type | desc | 79 | |-----------|----------|------------------------------| 80 | | `body` | `string` | Message content | 81 | | `sig` | `string` | ASCII-armored PGP signature | 82 | | `fprint` | `string` | Public key fingerprint (hex) | 83 | | `user` | `User` | User sending the message | 84 | -------------------------------------------------------------------------------- /docs/quick-start.md: -------------------------------------------------------------------------------- 1 | # Quick start 2 | 3 | ### Server 4 | `// TODO` 5 | 6 | ### Client 7 | `usage: boltchat [identity]` 8 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/boltchat 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/fatih/color v1.10.0 7 | github.com/gdamore/tcell/v2 v2.1.0 8 | github.com/magefile/mage v1.11.0 9 | github.com/mitchellh/mapstructure v1.4.1 10 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad 11 | gopkg.in/yaml.v2 v2.4.0 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= 2 | github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= 3 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 4 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 5 | github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= 6 | github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= 7 | github.com/gdamore/tcell/v2 v2.1.0 h1:UnSmozHgBkQi2PGsFr+rpdXuAPRRucMegpQp3Z3kDro= 8 | github.com/gdamore/tcell/v2 v2.1.0/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA= 9 | github.com/keesvv/addlicense v0.0.0-20210126202959-89a52823b8c3 h1:hiMS7ub2idKEHi3RLk0XAWMMHdMtTB+by42zHx2KHW4= 10 | github.com/keesvv/addlicense v0.0.0-20210126202959-89a52823b8c3/go.mod h1:Qn87eYN10woSc2wlJDXNlQYeGfrAcURL8ssQoytz2KM= 11 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 12 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 13 | github.com/magefile/mage v1.11.0 h1:C/55Ywp9BpgVVclD3lRnSYCwXTYxmSppIgLeDYlNuls= 14 | github.com/magefile/mage v1.11.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= 15 | github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= 16 | github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 17 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 18 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 19 | github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= 20 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 21 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 22 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 23 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 24 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= 25 | golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 26 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 27 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= 28 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 29 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= 30 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 31 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 32 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 34 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 36 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 37 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 38 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 41 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 42 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 43 | -------------------------------------------------------------------------------- /lib/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import ( 18 | "encoding/json" 19 | "net" 20 | 21 | "github.com/boltchat/client/identity" 22 | "github.com/boltchat/lib/pgp" 23 | "github.com/boltchat/protocol" 24 | "github.com/boltchat/protocol/events" 25 | ) 26 | 27 | type Client struct { 28 | Conn *net.TCPConn // TODO: make private 29 | Identity *identity.Identity 30 | Opts Options 31 | enc *json.Encoder 32 | dec *json.Decoder 33 | } 34 | 35 | func NewClient(opts Options) *Client { 36 | return &Client{ 37 | Identity: opts.Identity, 38 | Opts: opts, 39 | } 40 | } 41 | 42 | // TODO: refactor 43 | func (c *Client) Connect() error { 44 | var ip net.IP 45 | var port int = c.Opts.Port 46 | 47 | if parsedIP := net.ParseIP(c.Opts.Hostname); parsedIP != nil { 48 | ip = parsedIP 49 | } 50 | 51 | if ip == nil { 52 | _, srvAddrs, _ := net.LookupSRV("bolt", "tcp", c.Opts.Hostname) 53 | 54 | if len(srvAddrs) > 0 { 55 | targetIps, _ := net.LookupIP(srvAddrs[0].Target) 56 | ip = targetIps[0] 57 | port = int(srvAddrs[0].Port) 58 | } 59 | } 60 | 61 | if ip == nil { 62 | ips, lookupErr := net.LookupIP(c.Opts.Hostname) 63 | if lookupErr != nil { 64 | return lookupErr 65 | } 66 | 67 | ip = ips[0] 68 | } 69 | 70 | conn, dialErr := net.DialTCP("tcp", nil, &net.TCPAddr{ 71 | IP: ip, 72 | Port: port, 73 | }) 74 | 75 | if dialErr != nil { 76 | return dialErr 77 | } 78 | 79 | // Set the connection & decoders 80 | c.Conn = conn 81 | c.enc = json.NewEncoder(conn) 82 | c.dec = json.NewDecoder(conn) 83 | 84 | c.SendRaw(*events.NewJoinEvent(&protocol.User{ 85 | Nickname: c.Identity.Nickname, // TODO: 86 | PublicKey: pgp.ArmorPublicKey(c.Identity.Entity), 87 | })) 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /lib/client/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import "github.com/boltchat/protocol/events" 18 | 19 | func (c *Client) SendCommand(cmd *events.CommandData) error { 20 | return c.SendRaw(*events.NewCommandEvent(cmd.Command, cmd.Args)) 21 | } 22 | -------------------------------------------------------------------------------- /lib/client/events.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import ( 18 | "github.com/boltchat/protocol/events" 19 | ) 20 | 21 | func (c *Client) ReadEvents(evts chan *events.Event, closed chan bool) { 22 | for { 23 | evt := &events.Event{} 24 | err := c.ReceiveRaw(evt) 25 | 26 | if err != nil { 27 | closed <- true 28 | return 29 | } 30 | 31 | evts <- evt 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/client/message.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import ( 18 | "bytes" 19 | "strings" 20 | 21 | "github.com/boltchat/protocol" 22 | "github.com/boltchat/protocol/events" 23 | "golang.org/x/crypto/openpgp" 24 | "golang.org/x/crypto/openpgp/packet" 25 | ) 26 | 27 | /* 28 | SendMessage sends a message to an established 29 | TCP connection. 30 | */ 31 | func (c *Client) SendMessage(m *protocol.Message) error { 32 | c.SendRaw(*events.NewMessageEvent(m)) 33 | return nil 34 | } 35 | 36 | // SignMessage replaces the contents of a message with 37 | // an Identity signature with the original contents embedded. 38 | func (c *Client) SignMessage(m *protocol.Message) error { 39 | r := strings.NewReader(m.Content) 40 | buff := new(bytes.Buffer) 41 | 42 | err := openpgp.ArmoredDetachSignText(buff, c.Identity.Entity, r, &packet.Config{}) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | m.Signature = buff.String() 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /lib/client/options.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | import "github.com/boltchat/client/identity" 18 | 19 | // Options TODO 20 | type Options struct { 21 | Hostname string 22 | Port int 23 | Identity *identity.Identity 24 | } 25 | -------------------------------------------------------------------------------- /lib/client/util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package client 16 | 17 | func (c *Client) SendRaw(data interface{}) error { 18 | return c.enc.Encode(data) 19 | } 20 | 21 | func (c *Client) ReceiveRaw(out interface{}) error { 22 | return c.dec.Decode(out) 23 | } 24 | -------------------------------------------------------------------------------- /lib/pgp/armor.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pgp 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | 21 | "golang.org/x/crypto/openpgp" 22 | "golang.org/x/crypto/openpgp/armor" 23 | ) 24 | 25 | func ArmorPublicKey(entity *openpgp.Entity) string { 26 | armorBuf := new(bytes.Buffer) 27 | armorWriter, err := armor.Encode(armorBuf, openpgp.PublicKeyType, nil) 28 | 29 | if err != nil { 30 | fmt.Println(err) 31 | } 32 | 33 | err = entity.Serialize(armorWriter) 34 | 35 | if err != nil { 36 | fmt.Println(err) 37 | } 38 | 39 | armorWriter.Close() 40 | 41 | return armorBuf.String() 42 | } 43 | -------------------------------------------------------------------------------- /lib/pgp/create.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pgp 16 | 17 | import ( 18 | "os" 19 | 20 | "golang.org/x/crypto/openpgp" 21 | "golang.org/x/crypto/openpgp/packet" 22 | ) 23 | 24 | func WritePGPEntity(path string, entity *openpgp.Entity) error { 25 | // Create entity file 26 | f, fileErr := os.Create(path) 27 | defer f.Close() 28 | 29 | // Only the current user may access this file 30 | f.Chmod(0600) 31 | 32 | if fileErr != nil { 33 | return fileErr 34 | } 35 | 36 | // Serialize the entity (including private key) 37 | return entity.SerializePrivate(f, &packet.Config{}) 38 | } 39 | 40 | func CreatePGPEntity(nickname string) (*openpgp.Entity, error) { 41 | entity, entityErr := openpgp.NewEntity( 42 | nickname, 43 | "generated by Bolt", 44 | "identities@boltchat.net", 45 | &packet.Config{}, 46 | ) 47 | 48 | if entityErr != nil { 49 | return nil, entityErr 50 | } 51 | 52 | return entity, nil 53 | } 54 | -------------------------------------------------------------------------------- /lib/pgp/load.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pgp 16 | 17 | import ( 18 | "os" 19 | 20 | "golang.org/x/crypto/openpgp" 21 | "golang.org/x/crypto/openpgp/packet" 22 | ) 23 | 24 | func LoadPGPEntity(path string) (*openpgp.Entity, error) { 25 | f, openErr := os.Open(path) 26 | defer f.Close() 27 | 28 | if openErr != nil { 29 | return nil, openErr 30 | } 31 | 32 | pReader := packet.NewReader(f) 33 | entity, err := openpgp.ReadEntity(pReader) 34 | return entity, err 35 | } 36 | -------------------------------------------------------------------------------- /magefile.go: -------------------------------------------------------------------------------- 1 | // +build mage 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | 12 | "github.com/magefile/mage/mg" 13 | "github.com/magefile/mage/sh" 14 | ) 15 | 16 | const name string = "boltchat" 17 | const buildDir string = "build" 18 | 19 | const serverPrefix string = "server" 20 | const clientPrefix string = "client" 21 | 22 | const serverEntry string = "cmd/server/server.go" 23 | const clientEntry string = "cmd/client/client.go" 24 | 25 | type ( 26 | Build mg.Namespace 27 | Test mg.Namespace 28 | Docker mg.Namespace 29 | CI mg.Namespace 30 | Install mg.Namespace 31 | ) 32 | 33 | type BuildOptions struct { 34 | Static bool 35 | Extension string 36 | Prefix string 37 | } 38 | 39 | func build(os string, arch string, entry string, opts BuildOptions) error { 40 | env := map[string]string{ 41 | "GOOS": os, 42 | "GOARCH": arch, 43 | } 44 | 45 | // Build static binary 46 | if opts.Static { 47 | env["CGO_ENABLED"] = "0" 48 | } 49 | 50 | outputName := fmt.Sprintf( 51 | "%s-%s-%s-%s", name, opts.Prefix, os, arch, 52 | ) 53 | 54 | outputPath := path.Join( 55 | buildDir, 56 | outputName, 57 | ) 58 | 59 | if opts.Extension != "" { 60 | outputPath += fmt.Sprintf(".%s", opts.Extension) 61 | } 62 | 63 | args := []string{ 64 | "build", 65 | "-o", 66 | outputPath, 67 | "-ldflags", 68 | "-s -w", 69 | entry, 70 | } 71 | 72 | fmt.Println(args) 73 | 74 | return sh.RunWith( 75 | env, "go", args..., 76 | ) 77 | } 78 | 79 | /* 80 | Build 81 | */ 82 | 83 | // Builds all binaries 84 | func (Build) All() { 85 | mg.Deps( 86 | Build.ServerDarwinAmd64, 87 | Build.ServerLinuxAmd64, 88 | Build.ServerWindowsAmd64, 89 | 90 | Build.ClientDarwinAmd64, 91 | Build.ClientLinuxAmd64, 92 | Build.ClientWindowsAmd64, 93 | ) 94 | } 95 | 96 | // Builds the server binary for Linux (amd64) 97 | func (Build) ServerLinuxAmd64() error { 98 | return build("linux", "amd64", serverEntry, BuildOptions{Prefix: serverPrefix}) 99 | } 100 | 101 | // Builds the server binary for Windows (amd64) 102 | func (Build) ServerWindowsAmd64() error { 103 | return build("windows", "amd64", serverEntry, BuildOptions{ 104 | Extension: "exe", 105 | Prefix: serverPrefix, 106 | }) 107 | } 108 | 109 | // Builds the server binary for Darwin/macOS (amd64) 110 | func (Build) ServerDarwinAmd64() error { 111 | return build("darwin", "amd64", serverEntry, BuildOptions{Prefix: serverPrefix}) 112 | } 113 | 114 | // Builds the server binary for Darwin/macOS (arm64, M1) 115 | // func (Build) ServerDarwinArm64() error { 116 | // return build("darwin", "arm64", serverEntry, false) 117 | // } 118 | 119 | // Builds the static server binary for use in a Docker container 120 | func (Build) ServerStatic() error { 121 | return build("linux", "amd64", serverEntry, BuildOptions{ 122 | Static: true, 123 | Prefix: serverPrefix, 124 | }) 125 | } 126 | 127 | // Builds the client binary for Linux (amd64) 128 | func (Build) ClientLinuxAmd64() error { 129 | return build("linux", "amd64", clientEntry, BuildOptions{Prefix: clientPrefix}) 130 | } 131 | 132 | // Builds the client binary for Windows (amd64) 133 | func (Build) ClientWindowsAmd64() error { 134 | return build("windows", "amd64", clientEntry, BuildOptions{ 135 | Extension: "exe", 136 | Prefix: clientPrefix, 137 | }) 138 | } 139 | 140 | // Builds the client binary for Darwin/macOS (amd64) 141 | func (Build) ClientDarwinAmd64() error { 142 | return build("darwin", "amd64", clientEntry, BuildOptions{Prefix: clientPrefix}) 143 | } 144 | 145 | /* 146 | Test 147 | */ 148 | 149 | // Runs all unit tests 150 | func (Test) Unit() error { 151 | return sh.RunV("go", "test", "-v", "./...") 152 | } 153 | 154 | /* 155 | Docker 156 | */ 157 | 158 | // Builds a Docker image for the server 159 | func (Docker) Build() error { 160 | return sh.RunV("docker", "build", ".", "-t", name) 161 | } 162 | 163 | /* 164 | CI/CD 165 | */ 166 | 167 | // Compresses all binaries into a single tarball 168 | func (CI) CompressBinaries() error { 169 | return sh.Run("tar", "-cvzf", "binaries.tar.gz", "build") 170 | } 171 | 172 | /* 173 | Misc 174 | */ 175 | 176 | // Adds license headers to source files 177 | func License() { 178 | paths := make([]string, 0) 179 | 180 | filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 181 | if err != nil { 182 | return err 183 | } 184 | 185 | if strings.HasSuffix(path, ".go") { 186 | paths = append(paths, path) 187 | } 188 | return nil 189 | }) 190 | 191 | sh.Run( 192 | "addlicense", 193 | "-l", "apache", 194 | "-c", "The boltchat Authors", 195 | "client", "server", "protocol", "cmd", "util", "lib", 196 | ) 197 | } 198 | 199 | // Cleans up build directories 200 | func Clean() { 201 | sh.Rm("build") 202 | } 203 | 204 | // Installs the server to /usr/local/bin 205 | // (Linux-only) 206 | func (Install) Server() error { 207 | return sh.Run( 208 | "cp", 209 | "build/boltchat-server-linux-amd64", 210 | "/usr/local/bin/boltchat-server", 211 | ) 212 | } 213 | -------------------------------------------------------------------------------- /protocol/errs/errors.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package errs 16 | 17 | const ( 18 | InvalidEvent string = "invalid_event" 19 | InvalidFormat string = "invalid_format" 20 | Unidentified string = "unidentified" 21 | TooManyMessages string = "too_many_messages" 22 | SigVerifyFailed string = "sig_verification_failed" 23 | CommandNotFound string = "cmd_not_found" 24 | ) 25 | -------------------------------------------------------------------------------- /protocol/events/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package events 16 | 17 | // CommandType is the event type used for commands. 18 | const CommandType Type = "cmd" 19 | 20 | type CommandData struct { 21 | Command string `json:"cmd" mapstructure:"cmd"` 22 | Args []string `json:"args" mapstructure:"args"` 23 | } 24 | 25 | // NewCommandEvent TODO 26 | func NewCommandEvent(cmd string, args []string) *Event { 27 | return NewEvent(CommandType, CommandData{ 28 | Command: cmd, 29 | Args: args, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /protocol/events/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package events 16 | 17 | // ErrorType is the event type used for error reporting. 18 | const ErrorType Type = "err" 19 | 20 | type ErrorData struct { 21 | Error string `json:"err" mapstructure:"err"` 22 | } 23 | 24 | // NewErrorEvent TODO 25 | func NewErrorEvent(err string) *Event { 26 | return NewEvent(ErrorType, ErrorData{ 27 | Error: err, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /protocol/events/event.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package events 16 | 17 | import "time" 18 | 19 | // Type represents an event type identifier. 20 | type Type string 21 | 22 | // EventMeta represents the metadata that is 23 | // accompanied with each event. 24 | type EventMeta struct { 25 | // The event identifier/type. 26 | Type Type `json:"t"` 27 | // The event creation date (client-side, untrustworthy). 28 | CreatedAt int64 `json:"c"` 29 | } 30 | 31 | // Event represents a server event. 32 | type Event struct { 33 | Meta *EventMeta `json:"e"` 34 | Data interface{} `json:"d"` 35 | } 36 | 37 | // NewEvent TODO 38 | func NewEvent(t Type, data interface{}) *Event { 39 | return &Event{ 40 | Meta: &EventMeta{ 41 | Type: t, 42 | CreatedAt: time.Now().Unix(), 43 | }, 44 | Data: data, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /protocol/events/join.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package events 16 | 17 | import ( 18 | "github.com/boltchat/protocol" 19 | ) 20 | 21 | // JoinType is the event type used for new connections. 22 | const JoinType Type = "join" 23 | 24 | type JoinData struct { 25 | User *protocol.User `json:"user" mapstructure:"user"` 26 | } 27 | 28 | func NewJoinEvent(user *protocol.User) *Event { 29 | return NewEvent(JoinType, JoinData{ 30 | User: user, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /protocol/events/leave.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package events 16 | 17 | import ( 18 | "github.com/boltchat/protocol" 19 | ) 20 | 21 | // LeaveType is the event type used for disconnects. 22 | const LeaveType Type = "leave" 23 | 24 | type LeaveData struct { 25 | User *protocol.User `json:"user" mapstructure:"user"` 26 | } 27 | 28 | func NewLeaveEvent(user *protocol.User) *Event { 29 | return NewEvent(LeaveType, LeaveData{ 30 | User: user, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /protocol/events/message.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package events 16 | 17 | import "github.com/boltchat/protocol" 18 | 19 | // MessageType is the event type used for messages. 20 | const MessageType Type = "msg" 21 | 22 | type MessageData struct { 23 | Message *protocol.Message `json:"msg" mapstructure:"msg"` 24 | } 25 | 26 | // NewMessageEvent TODO 27 | func NewMessageEvent(msg *protocol.Message) *Event { 28 | return NewEvent(MessageType, MessageData{ 29 | Message: msg, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /protocol/events/motd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package events 16 | 17 | // MotdType is the event type used for the Message-of-the-Day (MOTD). 18 | const MotdType Type = "motd" 19 | 20 | type MotdData struct { 21 | Motd string `json:"motd" mapstructure:"motd"` 22 | } 23 | 24 | // NewMotdEvent constructs a new MotdEvent 25 | func NewMotdEvent(motd string) *Event { 26 | return NewEvent(MotdType, MotdData{ 27 | Motd: motd, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /protocol/events/notice.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package events 16 | 17 | const NoticeType Type = "notice" 18 | 19 | type NoticeData struct { 20 | Message string `json:"msg" mapstructure:"msg"` 21 | } 22 | 23 | func NewNoticeEvent(msg string) *Event { 24 | return NewEvent(NoticeType, NoticeData{ 25 | Message: msg, 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /protocol/message.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package protocol 16 | 17 | /* 18 | Message represents a message that is 19 | either transmitted or stored locally. 20 | */ 21 | type Message struct { 22 | Content string `json:"body" mapstructure:"body"` 23 | Signature string `json:"sig,omitempty" mapstructure:"sig"` 24 | Fingerprint string `json:"fprint,omitempty" mapstructure:"fprint"` 25 | User *User `json:"user" mapstructure:"user"` 26 | } 27 | -------------------------------------------------------------------------------- /protocol/user.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package protocol 16 | 17 | type User struct { 18 | Nickname string `json:"nick" mapstructure:"nick"` 19 | PublicKey string `json:"pubkey,omitempty" mapstructure:"pubkey"` 20 | } 21 | -------------------------------------------------------------------------------- /protocol/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package protocol 16 | 17 | import "github.com/boltchat/util/version" 18 | 19 | var Version = &version.Version{ 20 | Type: version.ProtocolType, 21 | VersionString: "0.2.0-alpha", 22 | } 23 | -------------------------------------------------------------------------------- /scripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd /app 3 | exec ./server 4 | -------------------------------------------------------------------------------- /server/commands/commands.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package commands 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/boltchat/server/pools" 21 | ) 22 | 23 | type CommandHandler func(p *pools.ConnPool, c *pools.Connection, args []string) 24 | 25 | var ErrCmdNotFound = errors.New("command not found") 26 | 27 | var commands = map[string]CommandHandler{ 28 | "ping": handlePing, 29 | } 30 | 31 | func Parse(cmd string) (CommandHandler, error) { 32 | cmdHandler, ok := commands[cmd] 33 | if !ok { 34 | return nil, ErrCmdNotFound 35 | } 36 | 37 | return cmdHandler, nil 38 | } 39 | -------------------------------------------------------------------------------- /server/commands/ping.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package commands 16 | 17 | import ( 18 | "github.com/boltchat/protocol/events" 19 | "github.com/boltchat/server/pools" 20 | ) 21 | 22 | func handlePing(p *pools.ConnPool, c *pools.Connection, args []string) { 23 | c.SendEvent(events.NewNoticeEvent("Pong!")) 24 | } 25 | -------------------------------------------------------------------------------- /server/handlers/command.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package handlers 16 | 17 | import ( 18 | "github.com/boltchat/protocol/errs" 19 | "github.com/boltchat/protocol/events" 20 | "github.com/boltchat/server/commands" 21 | "github.com/boltchat/server/pools" 22 | "github.com/mitchellh/mapstructure" 23 | ) 24 | 25 | func HandleCommand(p *pools.ConnPool, c *pools.Connection, e *events.Event) { 26 | cmdData := events.CommandData{} 27 | mapstructure.Decode(e.Data, &cmdData) 28 | 29 | cmdHandler, err := commands.Parse(cmdData.Command) 30 | if err != nil { 31 | c.SendError(errs.CommandNotFound) 32 | return 33 | } 34 | 35 | cmdHandler(p, c, cmdData.Args) 36 | } 37 | -------------------------------------------------------------------------------- /server/handlers/connect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package handlers 16 | 17 | import ( 18 | "github.com/boltchat/protocol/errs" 19 | "github.com/boltchat/protocol/events" 20 | "github.com/boltchat/server/pools" 21 | ) 22 | 23 | /* 24 | HandleConnection handles a TCP connection 25 | during its entire lifespan. 26 | */ 27 | func HandleConnection(pool *pools.ConnPool, conn *pools.Connection) { 28 | for { 29 | evt := &events.Event{} 30 | 31 | // Wait for and receive incoming events 32 | connErr := conn.Read(evt) 33 | 34 | if connErr != nil { 35 | // Broadcast a disconnect message 36 | pool.BroadcastEvent(events.NewLeaveEvent(conn.User)) 37 | pool.RemoveFromPool(conn) 38 | return 39 | } 40 | 41 | // TODO: 42 | // if err != nil { 43 | // conn.SendError(errs.InvalidFormat) 44 | // continue 45 | // } 46 | 47 | if !conn.IsIdentified() && evt.Meta.Type != events.JoinType { 48 | conn.SendError(errs.Unidentified) 49 | continue 50 | } 51 | 52 | // Get and execute the corresponding event handler 53 | evtHandler := GetHandler(evt.Meta.Type) 54 | evtHandler(pool, conn, evt) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /server/handlers/default.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package handlers 16 | 17 | import ( 18 | "github.com/boltchat/protocol/errs" 19 | "github.com/boltchat/protocol/events" 20 | "github.com/boltchat/server/pools" 21 | ) 22 | 23 | func HandleDefault(p *pools.ConnPool, c *pools.Connection, e *events.Event) { 24 | c.SendError(errs.InvalidEvent) 25 | } 26 | -------------------------------------------------------------------------------- /server/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package handlers 16 | 17 | import ( 18 | "github.com/boltchat/protocol/events" 19 | "github.com/boltchat/server/pools" 20 | ) 21 | 22 | type handler = func(p *pools.ConnPool, c *pools.Connection, e *events.Event) 23 | 24 | var handlerMap = map[events.Type]handler{ 25 | events.MessageType: HandleMessage, 26 | events.JoinType: HandleJoin, 27 | events.CommandType: HandleCommand, 28 | } 29 | 30 | func GetHandler(evtType events.Type) handler { 31 | if evtHandler, ok := handlerMap[evtType]; ok { 32 | return evtHandler 33 | } 34 | 35 | // Use default handler if event is not recognized 36 | return HandleDefault 37 | } 38 | -------------------------------------------------------------------------------- /server/handlers/join.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package handlers 16 | 17 | import ( 18 | "os" 19 | 20 | "github.com/boltchat/protocol/events" 21 | "github.com/boltchat/server/plugins" 22 | "github.com/boltchat/server/pools" 23 | "github.com/mitchellh/mapstructure" 24 | ) 25 | 26 | func HandleJoin(p *pools.ConnPool, c *pools.Connection, e *events.Event) { 27 | joinData := events.JoinData{} 28 | mapstructure.Decode(e.Data, &joinData) 29 | 30 | err := plugins.GetManager().HookIdentify(&joinData, c) 31 | if err != nil { 32 | c.SendError(err.Error()) 33 | return 34 | } 35 | 36 | c.User = joinData.User 37 | 38 | motd, hasMotd := os.LookupEnv("MOTD") // Get MOTD env 39 | if hasMotd { 40 | // Send MOTD if env var is declared 41 | c.SendEvent(events.NewMotdEvent(motd)) 42 | } 43 | 44 | p.BroadcastEvent(events.NewJoinEvent(joinData.User)) 45 | } 46 | -------------------------------------------------------------------------------- /server/handlers/message.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package handlers 16 | 17 | import ( 18 | "encoding/hex" 19 | 20 | "github.com/boltchat/protocol/errs" 21 | "github.com/boltchat/protocol/events" 22 | "github.com/boltchat/server/logging" 23 | "github.com/boltchat/server/pgp" 24 | "github.com/boltchat/server/plugins" 25 | "github.com/boltchat/server/pools" 26 | "github.com/mitchellh/mapstructure" 27 | ) 28 | 29 | func HandleMessage(p *pools.ConnPool, c *pools.Connection, e *events.Event) { 30 | msgData := events.MessageData{} 31 | mapstructure.Decode(e.Data, &msgData) 32 | err := plugins.GetManager().HookMessage(&msgData, c) 33 | 34 | if err != nil { 35 | c.SendError(err.Error()) 36 | return 37 | } 38 | 39 | pubKey, verifyErr := pgp.VerifyMessageSignature( 40 | msgData.Message.Signature, 41 | c.User.PublicKey, 42 | msgData.Message.Content, 43 | ) 44 | 45 | if verifyErr != nil { 46 | logging.LogDebug("Signature does not match.", nil) 47 | c.SendError(errs.SigVerifyFailed) 48 | return 49 | } 50 | 51 | logging.LogDebug("Signature matches.", nil) 52 | msgData.Message.Fingerprint = hex.EncodeToString(pubKey.Fingerprint[:]) 53 | p.BroadcastEvent(events.NewMessageEvent(msgData.Message)) 54 | } 55 | -------------------------------------------------------------------------------- /server/listener.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package server 16 | 17 | import ( 18 | "net" 19 | 20 | "github.com/boltchat/server/handlers" 21 | "github.com/boltchat/server/logging" 22 | "github.com/boltchat/server/pools" 23 | ) 24 | 25 | type Bind struct { 26 | Address string 27 | Proto string 28 | } 29 | 30 | // Listener TODO 31 | type Listener struct { 32 | Bind []Bind 33 | Port int 34 | } 35 | 36 | /* 37 | handleListener handles an individual TCP listener. 38 | */ 39 | func handleListener(pool *pools.ConnPool, l *net.TCPListener) error { 40 | for { 41 | tcpConn, err := l.AcceptTCP() 42 | conn := pools.NewConnection(tcpConn, nil) 43 | 44 | // Add connection to pool 45 | pool.AddToPool(conn) 46 | 47 | if err != nil { 48 | return err 49 | } 50 | 51 | // Accept new connection 52 | go handlers.HandleConnection(pool, conn) 53 | } 54 | } 55 | 56 | /* 57 | Listen starts a new server/listener. 58 | */ 59 | func (listener *Listener) Listen() error { 60 | // The connection pool for this listener 61 | connPool := make(pools.ConnPool, 0, 5) 62 | 63 | for _, bind := range listener.Bind { 64 | l, err := net.ListenTCP(bind.Proto, &net.TCPAddr{ 65 | IP: net.ParseIP(bind.Address), 66 | Port: listener.Port, 67 | }) 68 | 69 | if err != nil { 70 | return err 71 | } 72 | 73 | // TODO 74 | logging.LogListener(l.Addr().String()) 75 | 76 | go handleListener(&connPool, l) 77 | } 78 | 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /server/logging/logger.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package logging 16 | 17 | import ( 18 | "fmt" 19 | "os" 20 | "time" 21 | 22 | "github.com/boltchat/protocol" 23 | "github.com/boltchat/protocol/events" 24 | "github.com/fatih/color" 25 | ) 26 | 27 | type EventType int 28 | 29 | const ( 30 | RecvType EventType = iota 31 | SendType EventType = iota 32 | ) 33 | 34 | func logBase( 35 | level string, 36 | msg string, 37 | ) { 38 | fmt.Printf("%s %s %s\n", color.HiBlackString(time.Now().Format("15:04:05")), level, msg) 39 | } 40 | 41 | func LogInfo(msg string) { 42 | logBase(color.CyanString("INFO"), msg) 43 | } 44 | 45 | func LogError(msg string) { 46 | logBase(color.HiRedString("ERROR"), msg) 47 | } 48 | 49 | func LogDebug(msg string, data interface{}) { 50 | _, isDebug := os.LookupEnv("DEBUG") 51 | if !isDebug { 52 | return 53 | } 54 | 55 | if data != nil { 56 | msg = fmt.Sprintf("%s %v", msg, data) 57 | } 58 | 59 | logBase(color.HiYellowString("DEBUG"), msg) 60 | } 61 | 62 | func LogEvent(evtType EventType, user *protocol.User, evt *events.Event) { 63 | typeMap := map[EventType]string{ 64 | RecvType: color.HiCyanString("<--"), 65 | SendType: color.HiRedString("-->"), 66 | } 67 | 68 | nickname := "unknown" 69 | 70 | if user != nil { 71 | nickname = color.HiYellowString(user.Nickname) 72 | } 73 | 74 | logBase(color.HiMagentaString("EVENT"), fmt.Sprintf( 75 | "%s %s | %s | %s", 76 | typeMap[evtType], 77 | color.HiCyanString("%s", evt.Meta.Type), 78 | color.HiBlackString("user: %s", nickname), 79 | color.HiBlackString("data:\n")+fmt.Sprintf("%v", evt.Data), 80 | )) 81 | } 82 | 83 | // LogListener TODO 84 | func LogListener(addr string) { 85 | LogInfo(fmt.Sprintf("Server listening on %s.", addr)) 86 | } 87 | -------------------------------------------------------------------------------- /server/pgp/verify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pgp 16 | 17 | import ( 18 | "errors" 19 | "strings" 20 | 21 | "golang.org/x/crypto/openpgp/armor" 22 | "golang.org/x/crypto/openpgp/packet" 23 | ) 24 | 25 | func getSignature(rawSig string) (*packet.Signature, error) { 26 | sigReader := strings.NewReader(rawSig) 27 | sigDecoded, decodeErr := armor.Decode(sigReader) 28 | 29 | if decodeErr != nil { 30 | return nil, decodeErr 31 | } 32 | 33 | pack, packErr := packet.Read(sigDecoded.Body) 34 | 35 | if packErr != nil { 36 | return nil, packErr 37 | } 38 | 39 | sig, ok := pack.(*packet.Signature) 40 | if !ok { 41 | return nil, errors.New("invalid signature") 42 | } 43 | 44 | return sig, nil 45 | } 46 | 47 | func getPublicKey(rawPubKey string) (*packet.PublicKey, error) { 48 | pubKeyRead := strings.NewReader(rawPubKey) 49 | pubKeyDecode, pubKeyDecodeErr := armor.Decode(pubKeyRead) 50 | 51 | if pubKeyDecodeErr != nil { 52 | return nil, pubKeyDecodeErr 53 | } 54 | 55 | pubKeyPack, pubKeyPackErr := packet.Read(pubKeyDecode.Body) 56 | 57 | if pubKeyPackErr != nil { 58 | return nil, pubKeyPackErr 59 | } 60 | 61 | pubKey, ok := pubKeyPack.(*packet.PublicKey) 62 | if !ok { 63 | return nil, errors.New("invalid public key") 64 | } 65 | 66 | return pubKey, nil 67 | } 68 | 69 | // VerifyMessageSignature verifies a message sent by a user with 70 | // the corresponding public key and signature. 71 | func VerifyMessageSignature(rawSig string, rawPubKey string, msg string) (*packet.PublicKey, error) { 72 | sig, sigErr := getSignature(rawSig) 73 | if sigErr != nil { 74 | return nil, sigErr 75 | } 76 | 77 | pubKey, pubKeyErr := getPublicKey(rawPubKey) 78 | if pubKeyErr != nil { 79 | return nil, pubKeyErr 80 | } 81 | 82 | hash := sig.Hash.New() 83 | _, hashErr := hash.Write([]byte(msg)) 84 | 85 | if hashErr != nil { 86 | return nil, hashErr 87 | } 88 | 89 | return pubKey, pubKey.VerifySignature(hash, sig) 90 | } 91 | -------------------------------------------------------------------------------- /server/plugins/manager.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugins 16 | 17 | import ( 18 | "github.com/boltchat/protocol/events" 19 | "github.com/boltchat/server/pools" 20 | ) 21 | 22 | var manager *PluginManager 23 | 24 | type PluginManager struct { 25 | installedPlugins *[]Plugin 26 | } 27 | 28 | func (p *PluginManager) Install(plugins ...Plugin) { 29 | p.installedPlugins = &plugins 30 | } 31 | 32 | func (p *PluginManager) GetInstalled() *[]Plugin { 33 | return p.installedPlugins 34 | } 35 | 36 | func (p *PluginManager) HookMessage(msg *events.MessageData, conn *pools.Connection) error { 37 | for _, plugin := range *p.GetInstalled() { 38 | err := plugin.OnMessage(msg, conn) 39 | 40 | // Fail fast if a plugin reports an error 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (p *PluginManager) HookIdentify(data *events.JoinData, conn *pools.Connection) error { 50 | for _, plugin := range *p.GetInstalled() { 51 | err := plugin.OnIdentify(data, conn) 52 | 53 | // Fail fast if a plugin reports an error 54 | if err != nil { 55 | return err 56 | } 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func SetManager(mgr *PluginManager) { 63 | manager = mgr 64 | } 65 | 66 | func GetManager() *PluginManager { 67 | return manager 68 | } 69 | -------------------------------------------------------------------------------- /server/plugins/nickname_val.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugins 16 | 17 | import ( 18 | "errors" 19 | 20 | "github.com/boltchat/protocol/events" 21 | "github.com/boltchat/server/pools" 22 | ) 23 | 24 | type NicknameValidationPlugin struct { 25 | MinChars int 26 | MaxChars int 27 | } 28 | 29 | func (NicknameValidationPlugin) GetInfo() *PluginInfo { 30 | return &PluginInfo{ 31 | Id: "nickname-validation", 32 | } 33 | } 34 | 35 | var ErrNicknameTooShort = errors.New("nickname too short") 36 | var ErrNicknameTooLong = errors.New("nickname too long") 37 | 38 | func (p NicknameValidationPlugin) OnMessage(msg *events.MessageData, c *pools.Connection) error { 39 | return nil 40 | } 41 | 42 | func (p NicknameValidationPlugin) OnIdentify(data *events.JoinData, c *pools.Connection) error { 43 | if len(data.User.Nickname) < p.MinChars { 44 | return ErrNicknameTooShort 45 | } 46 | 47 | if len(data.User.Nickname) > p.MaxChars { 48 | return ErrNicknameTooLong 49 | } 50 | 51 | return nil 52 | } 53 | -------------------------------------------------------------------------------- /server/plugins/plugins.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugins 16 | 17 | import ( 18 | "github.com/boltchat/protocol/events" 19 | "github.com/boltchat/server/pools" 20 | ) 21 | 22 | type Plugin interface { 23 | OnMessage(msg *events.MessageData, c *pools.Connection) error 24 | OnIdentify(data *events.JoinData, c *pools.Connection) error 25 | GetInfo() *PluginInfo 26 | } 27 | 28 | type PluginInfo struct { 29 | Id string 30 | } 31 | -------------------------------------------------------------------------------- /server/plugins/rate_limiter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package plugins 16 | 17 | import ( 18 | "errors" 19 | "time" 20 | 21 | "github.com/boltchat/protocol/errs" 22 | "github.com/boltchat/protocol/events" 23 | "github.com/boltchat/server/pools" 24 | ) 25 | 26 | type RateLimiterPlugin struct { 27 | Amount int 28 | Time time.Duration 29 | } 30 | 31 | func (RateLimiterPlugin) GetInfo() *PluginInfo { 32 | return &PluginInfo{ 33 | Id: "rate-limiter", 34 | } 35 | } 36 | 37 | func (p RateLimiterPlugin) OnMessage(msg *events.MessageData, c *pools.Connection) error { 38 | const amountKey string = "rate:a" 39 | const timeKey string = "rate:t" 40 | 41 | now := time.Now() 42 | 43 | if c.Data[amountKey] == nil { 44 | c.Data[amountKey] = 0 45 | } 46 | 47 | if c.Data[timeKey] == nil { 48 | c.Data[timeKey] = now 49 | } 50 | 51 | elapsed := now.Sub(c.Data[timeKey].(time.Time)) 52 | 53 | if elapsed > p.Time { 54 | c.Data[timeKey] = now 55 | c.Data[amountKey] = 0 56 | } else if c.Data[amountKey].(int) >= p.Amount { 57 | return errors.New(errs.TooManyMessages) 58 | } else { 59 | c.Data[amountKey] = c.Data[amountKey].(int) + 1 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func (p RateLimiterPlugin) OnIdentify(data *events.JoinData, c *pools.Connection) error { 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /server/pools/connection.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pools 16 | 17 | import ( 18 | "encoding/json" 19 | "net" 20 | 21 | "github.com/boltchat/protocol" 22 | "github.com/boltchat/protocol/events" 23 | "github.com/boltchat/server/logging" 24 | ) 25 | 26 | // Connection TODO 27 | type Connection struct { 28 | Conn *net.TCPConn 29 | User *protocol.User 30 | Data map[string]interface{} 31 | encoder *json.Encoder 32 | decoder *json.Decoder 33 | } 34 | 35 | // NewConnection TODO 36 | func NewConnection(conn *net.TCPConn, user *protocol.User) *Connection { 37 | enc := json.NewEncoder(conn) 38 | dec := json.NewDecoder(conn) 39 | 40 | return &Connection{ 41 | Conn: conn, 42 | User: user, 43 | Data: make(map[string]interface{}, 0), 44 | encoder: enc, 45 | decoder: dec, 46 | } 47 | } 48 | 49 | // Send TODO 50 | func (c *Connection) Send(data interface{}) error { 51 | return c.encoder.Encode(data) 52 | } 53 | 54 | // SendEvent TODO 55 | func (c *Connection) SendEvent(evt *events.Event) error { 56 | err := c.Send(evt) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // Log the incoming event 62 | logging.LogEvent(logging.SendType, c.User, evt) 63 | 64 | return nil 65 | } 66 | 67 | func (c *Connection) Read(out *events.Event) error { 68 | err := c.decoder.Decode(out) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | // Log the incoming event 74 | logging.LogEvent(logging.RecvType, c.User, out) 75 | 76 | return nil 77 | } 78 | 79 | // SendError TODO 80 | func (c *Connection) SendError(err string) error { 81 | return c.SendEvent(events.NewErrorEvent(err)) 82 | } 83 | 84 | // Close closes the connection. 85 | func (c *Connection) Close() error { 86 | return c.Conn.Close() 87 | } 88 | 89 | // IsIdentified TODO 90 | func (c *Connection) IsIdentified() bool { 91 | return c.User != nil 92 | } 93 | -------------------------------------------------------------------------------- /server/pools/connpool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package pools 16 | 17 | import ( 18 | "github.com/boltchat/protocol/events" 19 | "github.com/boltchat/server/logging" 20 | ) 21 | 22 | // ConnPool represents a group of connections. 23 | type ConnPool []*Connection 24 | 25 | func (c *ConnPool) logPoolSize() { 26 | logging.LogDebug("pool size:", len(*c)) 27 | } 28 | 29 | // AddToPool adds a new connection to the current pool. 30 | func (c *ConnPool) AddToPool(conn *Connection) { 31 | *c = append(*c, conn) 32 | 33 | logging.LogDebug( 34 | "connection added to pool:", 35 | conn.Conn.RemoteAddr().String(), 36 | ) 37 | 38 | c.logPoolSize() 39 | } 40 | 41 | // RemoveFromPool removes an existing connection 42 | // from the current pool. 43 | func (c *ConnPool) RemoveFromPool(conn *Connection) { 44 | // Range through pool 45 | for i, curConn := range *c { 46 | // Target connection is found 47 | if curConn == conn { 48 | /* 49 | This removes the connection from the pool by its 50 | respective index. 51 | 52 | `i` represents the index of the matched connection. 53 | The first arg represents a slice of the pool that 54 | ends at the index of the connection in question. 55 | The second arg represents a slice of the pool that 56 | starts from the index of the connection in question + 1. 57 | */ 58 | *c = append((*c)[:i], (*c)[i+1:]...) 59 | break 60 | } 61 | } 62 | 63 | logging.LogDebug( 64 | "connection removed from pool:", 65 | conn.Conn.RemoteAddr().String(), 66 | ) 67 | 68 | c.logPoolSize() 69 | } 70 | 71 | // Broadcast emits data to all connections that are 72 | // present in the pool. 73 | func (c *ConnPool) Broadcast(data interface{}) { 74 | for _, conn := range *c { 75 | conn.Send(data) 76 | } 77 | } 78 | 79 | // BroadcastEvent emits an event to all connections that are 80 | // present in the pool. 81 | func (c *ConnPool) BroadcastEvent(evt *events.Event) { 82 | for _, conn := range *c { 83 | conn.SendEvent(evt) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/util/banner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package util 16 | 17 | import ( 18 | "fmt" 19 | "strings" 20 | ) 21 | 22 | // PrintBanner prints a neat ASCII art banner to stdout 23 | func PrintBanner() { 24 | ascii := strings.Join([]string{ 25 | " _ _ _ _ _ ", 26 | "| | | | | | | | | ", 27 | "| |__ ___ | | |_ ___| |__ __ _| |_ ", 28 | "| '_ \\ / _ \\| | __| / __| '_ \\ / _` | __|", 29 | "| |_) | (_) | | |_ _ | (__| | | | (_| | |_ ", 30 | "|_.__/ \\___/|_|\\__| (_) \\___|_| |_|\\__,_|\\__|", 31 | }, "\n") 32 | 33 | // Format & print the banner 34 | fmt.Printf("\n%s\n\n\n", ascii) 35 | } 36 | -------------------------------------------------------------------------------- /util/version/format.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | import ( 18 | "fmt" 19 | ) 20 | 21 | func FormatVersion(versions []*Version) string { 22 | str := "Copyright (c) 2021 The boltchat Authors\n" 23 | 24 | for i, v := range versions { 25 | str += fmt.Sprintf("%s version %s", v.Type, v.VersionString) 26 | 27 | if i < len(versions)-1 { 28 | str += "\n" 29 | } 30 | } 31 | 32 | return str 33 | } 34 | -------------------------------------------------------------------------------- /util/version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 The boltchat Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package version 16 | 17 | type VersionType string 18 | 19 | const ( 20 | ClientType VersionType = "client" 21 | ServerType VersionType = "server" 22 | ProtocolType VersionType = "protocol" 23 | ) 24 | 25 | type Version struct { 26 | Type VersionType 27 | VersionString string 28 | } 29 | --------------------------------------------------------------------------------