├── .github ├── ISSUE_TEMPLATE │ ├── Bug Report.yml │ └── Feature Request.yml └── workflows │ ├── build-packages.yaml │ ├── publish-docs.yaml │ ├── release-meshchat.yaml │ ├── workflow-meshchat-api-package.yaml │ └── workflow-meshchat-package.yaml ├── .gitignore ├── .release-it.yaml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── api └── meshchat ├── docs ├── History.md ├── Install.md └── Troubleshooting.md ├── luadox.conf ├── meshchat ├── meshchatconfig.lua ├── meshchatlib.lua ├── package ├── ipk-build.sh ├── meshchat-api │ └── control ├── meshchat │ ├── control │ ├── postinst │ ├── preinst │ └── prerm ├── populate-meshchat-api-fs.sh ├── populate-meshchat-fs.sh └── update-version.sh ├── support ├── meshchatsync └── meshchatsync-init.d └── www ├── alert.mp3 ├── chat.js ├── files.html ├── files.js ├── index.html ├── jquery-2.2.0.min.js ├── js.cookie.js ├── md5.js ├── messages.js ├── normalize.css ├── numeral.min.js ├── ohsnap.js ├── shared.js ├── skeleton.css ├── status.html ├── status.js └── style.css /.github/ISSUE_TEMPLATE/Bug Report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug for the MeshChat project 3 | title: "[Bug]: " 4 | labels: 5 | - bug 6 | - needs triage 7 | assignees: 8 | - hickey 9 | body: 10 | - type: markdown 11 | attributes: 12 | value: | 13 | Thank you for taking the time to create a bug report. Please 14 | attempt to fill in as much information as you are able to. 15 | - type: input 16 | id: contact 17 | attributes: 18 | label: Contact Details 19 | description: How can we get in touch with you if we need more info? 20 | placeholder: ex. email@example.com 21 | validations: 22 | required: false 23 | - type: dropdown 24 | id: version 25 | attributes: 26 | label: Version 27 | description: Version of MeshChat? 28 | options: 29 | - v1.x 30 | - v2.0 - v2.8 31 | - v2.9 32 | - v2.10 33 | - v2.12.0 34 | - development build (include version in what happened) 35 | default: 0 36 | validations: 37 | required: true 38 | - type: dropdown 39 | id: system_type 40 | attributes: 41 | label: System Type 42 | description: What type of system is MeshChat installed on? 43 | options: 44 | - AREDN node 45 | - Linux 46 | - Unknown 47 | default: 0 48 | validations: 49 | required: true 50 | - type: textarea 51 | id: what-happened 52 | attributes: 53 | label: What happened? 54 | description: Also tell us, what did you expect to happen? 55 | placeholder: | 56 | Describe to the best of your ability what happened or what you 57 | did to trigger the problem. 58 | validations: 59 | required: true 60 | - type: textarea 61 | id: config 62 | attributes: 63 | label: MeshChat configuration 64 | description: | 65 | If you are the admin of the MeshChat instance, it is asked that 66 | you past your MeshChat configuration file between the back ticks 67 | to aid in troubleshooting. 68 | value: | 69 | ``` 70 | 71 | ``` 72 | - type: dropdown 73 | id: connection_type 74 | attributes: 75 | label: Connection type 76 | multiple: false 77 | description: | 78 | How is the node that is running the MeshChat instance connected? 79 | If you know the mesh network that the node is connected to please 80 | indicate the name of the mesh network below in the node name field. 81 | options: 82 | - Non-connected mesh network 83 | - Mesh network connected through IP tunnel 84 | - Mesh network connected through a supernode 85 | - I don't know 86 | - type: input 87 | id: node_name 88 | attributes: 89 | label: Node name 90 | description: Please specify the node name where MeshChat runs. 91 | - type: dropdown 92 | id: browsers 93 | attributes: 94 | label: What browsers are you seeing the problem on? 95 | multiple: true 96 | options: 97 | - Firefox 98 | - Chrome 99 | - Safari 100 | - Microsoft Edge 101 | - Brave 102 | - Vivialdi 103 | - Other 104 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature Request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Looking to add an enhancement to the MeshChat project 3 | title: "[Feature]: " 4 | labels: 5 | - enhancement 6 | assignees: 7 | - hickey 8 | body: 9 | - type: markdown 10 | attributes: 11 | value: | 12 | Thank you for taking the time to let us know about your idea. Please 13 | attempt to fill in as much information as you are able to. 14 | - type: input 15 | id: contact 16 | attributes: 17 | label: Contact Details 18 | description: How can we get in touch with you if we need more info? 19 | placeholder: ex. email@example.com 20 | validations: 21 | required: false 22 | - type: dropdown 23 | id: enhancement_type 24 | attributes: 25 | label: Enhancement Type 26 | description: What sort of enhancement is this? 27 | options: 28 | - Graphical interface 29 | - Message formatting 30 | - File sharing 31 | - API improvements 32 | - Documentation 33 | - Installation method 34 | - Other 35 | default: 0 36 | validations: 37 | required: true 38 | - type: textarea 39 | id: description 40 | attributes: 41 | label: What is your idea or what can be improved? 42 | description: Please be descriptive so that we can better understand the enhancement. 43 | placeholder: Tell us your idea. 44 | validations: 45 | required: true 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/build-packages.yaml: -------------------------------------------------------------------------------- 1 | name: Build MeshChat Packages 2 | on: push 3 | 4 | jobs: 5 | calculate-version: 6 | runs-on: ubuntu-latest 7 | outputs: 8 | build_version: ${{ steps.build-version-slug.outputs.build_version }} 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - id: build-version-slug 14 | run: | 15 | date=$(date +%Y%m%d) 16 | branch="${GITHUB_REF_NAME}" 17 | commit=$(git rev-parse --short HEAD) 18 | version="${date}-${branch}-${commit}" 19 | 20 | echo "build_version=$version" >> $GITHUB_OUTPUT 21 | 22 | build-meshchat-package: 23 | needs: calculate-version 24 | uses: 25 | ./.github/workflows/workflow-meshchat-package.yaml 26 | with: 27 | build_version: ${{ needs.calculate-version.outputs.build_version }} 28 | build_dir: package/meshchat-ipkg 29 | 30 | build-meshchat-api-package: 31 | needs: calculate-version 32 | uses: 33 | ./.github/workflows/workflow-meshchat-api-package.yaml 34 | with: 35 | build_version: ${{ needs.calculate-version.outputs.build_version }} 36 | build_dir: package/meshchat-ipkg 37 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Publish MeshChat Documentation 2 | on: 3 | workflow_call: 4 | inputs: 5 | build_version: 6 | required: true 7 | type: string 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: jtackaberry/luadox:latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | ref: release 19 | - run: luadox -c luadox.conf 20 | - name: Fix permissions 21 | run: | 22 | chmod -c -R +rX "_site/" | while read line; do 23 | echo "::warning title=Invalid file permissions automatically fixed::$line" 24 | done 25 | - name: Update version strings 26 | run: | 27 | find docs -type f -exec sed -i "s/%VERSION%/${{ inputs.build_version }}/" {} \; 28 | - run: | 29 | echo ::group::Archive artifact 30 | tar -C "_site" \ 31 | -cvf "$RUNNER_TEMP/artifact.tar" \ 32 | --exclude=.git \ 33 | --exclude=.github \ 34 | . 35 | echo ::endgroup:: 36 | - name: Upload artifact 37 | id: upload-artifact 38 | uses: actions/upload-artifact@v4 39 | with: 40 | name: github-pages 41 | path: ${{ runner.temp }}/artifact.tar 42 | retention-days: 1 43 | if-no-files-found: error 44 | 45 | # Deploy job 46 | deploy: 47 | needs: build 48 | 49 | # Grant GITHUB_TOKEN the permissions required to make a Pages deployment 50 | permissions: 51 | pages: write # to deploy to Pages 52 | id-token: write # to verify the deployment originates from an appropriate source 53 | 54 | # Deploy to the github-pages environment 55 | environment: 56 | name: github-pages 57 | url: ${{ steps.deployment.outputs.page_url }} 58 | 59 | # Specify runner + deployment step 60 | runs-on: ubuntu-latest 61 | steps: 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action 65 | -------------------------------------------------------------------------------- /.github/workflows/release-meshchat.yaml: -------------------------------------------------------------------------------- 1 | name: Release MeshChat Package 2 | on: workflow_dispatch 3 | 4 | jobs: 5 | create-release: 6 | runs-on: ubuntu-latest 7 | # container: 8 | # image: registry.gitlab.com/wt0f/gitlab-runner-images/node:latest 9 | outputs: 10 | build_version: ${{ steps.detect_version.outputs.build_version }} 11 | env: 12 | GITHUB_TOKEN: ${{ secrets.RELEASE_IT_TOKEN }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | ref: release 18 | - name: git config 19 | run: | 20 | git config user.name "${GITHUB_ACTOR}" 21 | git config user.email "${GITHUB_ACTOR}@users.noreply.github.com" 22 | - run: npm install -g release-it @release-it/conventional-changelog @commitlint/config-conventional @commitlint/cli auto-changelog 23 | - id: detect_version 24 | run: echo "build_version=$(npx release-it --release-version)" >> $GITHUB_OUTPUT 25 | - run: npx release-it -VV --ci 26 | # - run: git checkout master 27 | # - run: git rebase release 28 | # - run: git push 29 | 30 | build-meshchat-package: 31 | needs: create-release 32 | uses: 33 | ./.github/workflows/workflow-meshchat-package.yaml 34 | with: 35 | ref: release 36 | build_version: ${{ needs.create-release.outputs.build_version }} 37 | build_dir: package/meshchat-ipkg 38 | 39 | build-meshchat-api-package: 40 | needs: create-release 41 | uses: 42 | ./.github/workflows/workflow-meshchat-api-package.yaml 43 | with: 44 | build_version: ${{ needs.create-release.outputs.build_version }} 45 | build_dir: package/meshchat-ipkg 46 | 47 | add-meshchat-package-to-release: 48 | needs: 49 | - build-meshchat-package 50 | - build-meshchat-api-package 51 | # container: 52 | # image: registry.gitlab.com/wt0f/gitlab-runner-images/node:latest 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v4 56 | with: 57 | fetch-depth: 0 58 | fetch-tags: true 59 | - run: git pull 60 | - run: npm install -g release-it @release-it/conventional-changelog @commitlint/config-conventional @commitlint/cli auto-changelog 61 | - uses: actions/download-artifact@v4 62 | with: 63 | name: ${{ needs.release_meshchat_package.outputs.package_file }} 64 | path: ${{ needs.release_meshchat_package.outputs.package_file }} 65 | - run: | 66 | for file in *.ipk; do 67 | echo "uploading $file" 68 | npx release-it --ci --no-increment --no-git --no-npm --github.update=true --github.assets=$file 69 | done 70 | env: 71 | GITHUB_TOKEN: ${{ secrets.RELEASE_IT_TOKEN }} 72 | 73 | update-documentation: 74 | needs: create-release 75 | uses: 76 | ./.github/workflows/publish-docs.yaml 77 | with: 78 | build_version: ${{ needs.create-release.outputs.build_version }} 79 | -------------------------------------------------------------------------------- /.github/workflows/workflow-meshchat-api-package.yaml: -------------------------------------------------------------------------------- 1 | name: Build MeshChat API Package 2 | on: 3 | workflow_call: 4 | inputs: 5 | build_version: 6 | required: true 7 | type: string 8 | build_dir: 9 | required: true 10 | type: string 11 | ref: 12 | required: false 13 | type: string 14 | default: ${{ github.ref_name }} 15 | 16 | jobs: 17 | create-meshchat-api-package: 18 | runs-on: ubuntu-latest 19 | # container: 20 | # image: registry.gitlab.com/wt0f/gitlab-runner-images/shell:latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | ref: ${{ inputs.ref }} 26 | - run: echo ${{ inputs.build_version }} > VERSION 27 | - run: package/populate-meshchat-api-fs.sh ${{ inputs.build_dir }} 28 | - run: package/update-version.sh ${{ inputs.build_dir }} 29 | - run: package/ipk-build.sh ${{ inputs.build_dir }} 30 | - id: detect-package-file 31 | run: echo "file=$(ls -1 meshchat-api_*.ipk)" >> $GITHUB_OUTPUT 32 | - run: echo "${{ steps.detect-package-file.outputs.file }}" 33 | - uses: actions/upload-artifact@v4 34 | with: 35 | name: ${{ steps.detect-package-file.outputs.file }} 36 | path: ${{ steps.detect-package-file.outputs.file }} 37 | -------------------------------------------------------------------------------- /.github/workflows/workflow-meshchat-package.yaml: -------------------------------------------------------------------------------- 1 | name: Build MeshChat Package 2 | on: 3 | workflow_call: 4 | inputs: 5 | build_version: 6 | required: true 7 | type: string 8 | build_dir: 9 | required: true 10 | type: string 11 | ref: 12 | required: false 13 | type: string 14 | default: ${{ github.ref_name }} 15 | 16 | jobs: 17 | create-meshchat-package: 18 | runs-on: ubuntu-latest 19 | # container: 20 | # image: registry.gitlab.com/wt0f/gitlab-runner-images/shell:latest 21 | outputs: 22 | package_file: ${{ steps.detect-package-file.outputs.file }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | ref: ${{ inputs.ref }} 28 | # - run: info "Populating the filesystem with MeshChat files" 29 | - run: echo ${{ inputs.build_version }} > VERSION 30 | - run: package/populate-meshchat-fs.sh ${{ inputs.build_dir }} 31 | # - run: info "Updating version numbers to " 32 | - run: package/update-version.sh ${{ inputs.build_dir }} 33 | # - run: info "Packing up MeshChat files" 34 | - run: package/ipk-build.sh ${{ inputs.build_dir }} 35 | - id: detect-package-file 36 | run: echo "file=$(ls -1 meshchat_*.ipk)" >> $GITHUB_OUTPUT 37 | - run: echo "${{ steps.detect-package-file.outputs.file }}" 38 | - uses: actions/upload-artifact@v4 39 | with: 40 | name: ${{ steps.detect-package-file.outputs.file }} 41 | path: ${{ steps.detect-package-file.outputs.file }} 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/.markupserve_index 2 | -------------------------------------------------------------------------------- /.release-it.yaml: -------------------------------------------------------------------------------- 1 | git: 2 | commit: true 3 | commitMessage: "chore(release): ${version}" 4 | commitArgs: "" 5 | tag: true 6 | tagName: "v${version}" 7 | tagAnnotation: "Automated release: ${version}" 8 | push: true 9 | requireBranch: release 10 | requireCommits: true 11 | changelog: "npx auto-changelog --stdout --commit-limit false" 12 | 13 | github: 14 | release: true 15 | releaseName: "v${version}" 16 | 17 | npm: 18 | publish: false 19 | 20 | plugins: 21 | "@release-it/conventional-changelog": 22 | infile: CHANGELOG.md 23 | preset: 24 | name: conventionalcommits 25 | types: 26 | - type: feat 27 | section: Features 28 | - type: fix 29 | section: Bug Fixes 30 | - tyep: docs 31 | section: Documentation 32 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## [2.12.1](https://github.com/hickey/meshchat/compare/v2.12.0...v2.12.1) (2024-08-20) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * Update zone name discovery to ignore icon metadata ([#52](https://github.com/hickey/meshchat/issues/52)) ([5c3f03a](https://github.com/hickey/meshchat/commit/5c3f03a31ad0d7b96f9e01d9785a7ebef2bd64f2)) 9 | 10 | ## [2.12.0](https://github.com/hickey/meshchat/compare/v2.11.2...v2.12.0) (2024-03-03) 11 | 12 | 13 | ### Features 14 | 15 | * add message class abstracting message handling ([#23](https://github.com/hickey/meshchat/issues/23)) ([28d1759](https://github.com/hickey/meshchat/commit/28d17592f7ee1a778fdd0bc740f451646493f9dd)) 16 | * Add support for meshchat_local.lua ([#47](https://github.com/hickey/meshchat/issues/47)) ([b51bb16](https://github.com/hickey/meshchat/commit/b51bb16baa0abf7f72a895471712e0bdb6d7a44d)) 17 | * allow admin to set default channel ([#19](https://github.com/hickey/meshchat/issues/19)) ([d90fc33](https://github.com/hickey/meshchat/commit/d90fc33eafecd31fec2dc825d8388f8e98ae8fd0)) 18 | * Set send channel when channel filter changed ([f26130b](https://github.com/hickey/meshchat/commit/f26130ba5f06f9b4fbd26257be8d967a61e531bc)) 19 | 20 | 21 | ### Bug Fixes 22 | 23 | * recover code lost from bad merges ([9ad1637](https://github.com/hickey/meshchat/commit/9ad163784ba66b1b42ec0b695a37104c3d8a348d)) 24 | * remove duplicate config definitions ([57d2766](https://github.com/hickey/meshchat/commit/57d2766d1bece87138ff4b76053e3316955454a0)) 25 | * Set caller to unknown_caller for notified functions ([7856502](https://github.com/hickey/meshchat/commit/78565026aae76dee20751e483f38f0bc7bfb94ad)) 26 | * set epoch in send_message API even if not specified ([bfadccb](https://github.com/hickey/meshchat/commit/bfadccb3f8010c51374e206d78585b22ac86848a)) 27 | 28 | ## [2.11.2](https://github.com/hickey/meshchat/compare/v2.11.1...v2.11.2) (2024-03-02) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * recover code lost from bad merges ([05c8dff](https://github.com/hickey/meshchat/commit/05c8dff7192941651145a4423916de46797adf3e)) 34 | * remove duplicate config definitions ([53dd0c6](https://github.com/hickey/meshchat/commit/53dd0c6785167537430e592e0d6d895499ac3ce7)) 35 | 36 | ## [2.11.1](https://github.com/hickey/meshchat/compare/v2.11.0...v2.11.1) (2024-03-02) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * remove duplicate config definitions ([#46](https://github.com/hickey/meshchat/issues/46)) ([a6cb468](https://github.com/hickey/meshchat/commit/a6cb468ca0fa594629a1bae0e22781dfc0bf074e)) 42 | 43 | ## [2.11.0](https://github.com/hickey/meshchat/compare/v2.10.0...v2.11.0) (2024-03-01) 44 | 45 | 46 | ### Features 47 | 48 | * allow admin to set default channel ([#19](https://github.com/hickey/meshchat/issues/19)) ([a1374f0](https://github.com/hickey/meshchat/commit/a1374f03da7d6cad218bee3e8486c707141f184c)) 49 | * Add documentation (#44) 50 | * Add message class abstracting message handling (#23) 51 | * Set send channel when channel filter changed 52 | 53 | ### Bug fixes 54 | 55 | * Discover zone name from /etc/config.mesh (#36) 56 | * set epoch in send_message API even if not specified 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MeshChat 2 | 3 | MeshChat for AREDN (in Lua). MeshChat has become the defacto standard 4 | chat application for AREDN networks. A number of features make it easy 5 | to implement and use: 6 | 7 | * Installable on AREDN firmware and most any Linux distribution 8 | * Automatic synchronization between nodes using the same service name 9 | * No account creation necessary--users access using call sign 10 | * Simple user interface 11 | 12 | If you are looking for a feature to be implemented or find a bug, please 13 | be sure to [create an issue](https://github.com/hickey/meshchat/issues/new) 14 | in the project so that it can be prioritized. 15 | 16 | Current documentation for MeshChat can be found at: 17 | 18 | https://hickey.github.io/meshchat 19 | -------------------------------------------------------------------------------- /api/meshchat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | --[[ 3 | 4 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks 5 | Copyright (C) 2022 Tim Wilkinson 6 | Base on code (C) Trevor Paskett (see https://github.com/tpaskett) 7 | See Contributors file for additional contributors 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation version 3 of the License. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | Additional Terms: 22 | 23 | Additional use restrictions exist on the AREDN(TM) trademark and logo. 24 | See AREDNLicense.txt for more info. 25 | 26 | Attributions to the AREDN Project must be retained in the source code. 27 | If importing this code into a new or existing project attribution 28 | to the AREDN project must be added to the source code. 29 | 30 | You must not misrepresent the origin of the material contained within. 31 | 32 | Modified versions must be modified to attribute to the original source 33 | and be marked in reasonable ways as differentiate it from the original 34 | version 35 | 36 | --]] 37 | 38 | require('nixio') 39 | require('luci.http') 40 | 41 | function str_escape(str) 42 | return str:gsub("%(", "%%("):gsub("%)", "%%)"):gsub("%%", "%%%%"):gsub("%.", "%%."):gsub("%+", "%%+"):gsub("-", "%%-"):gsub("%*", "%%*"):gsub("%[", "%%["):gsub("%?", "%%?"):gsub("%^", "%%^"):gsub("%$", "%%$") 43 | end 44 | 45 | local query = {} 46 | if os.getenv("QUERY_STRING") ~= "" then 47 | query = luci.http.Request(nixio.getenv()):formvalue() 48 | end 49 | 50 | print("Content-type: text/plain\r") 51 | print("\r") 52 | if query.action == "meshchat_nodes" then 53 | local pattern = "http://(%S+):(%d+)/meshchat|tcp|" .. str_escape(query.zone_name) .. "%s" 54 | for line in io.lines("/var/run/services_olsr") 55 | do 56 | local node, port = line:match(pattern) 57 | if node and port then 58 | print(node .. "\t" .. port) 59 | end 60 | end 61 | else 62 | print("error no action") 63 | end 64 | -------------------------------------------------------------------------------- /docs/History.md: -------------------------------------------------------------------------------- 1 | # History of MeshChat 2 | 3 | This is the history of the various MeshChat versions that have existed--at 4 | least to the best of my knowledge. 5 | 6 | ## MeshChat v0.4 - v1.02 7 | 8 | This was the original version of MeshChat written by Trevor Paskett (K7FPV) 9 | around 2015. It was written in Perl and worked well on the limited resources 10 | of the AREDN nodes. Around 2018 Trevor was not able to or not interested 11 | in supporting MeshChat any longer, it is unclear which but the project 12 | became stagnant at version v1.01 in August of 2018. There was a final 13 | release of v1.02 in September 2022 that mostly added a few patches and 14 | support for Debian Stretch. 15 | 16 | The K7FPV code base still exists at https://github.com/tpaskett/meshchat. 17 | 18 | In addition Trevor wrote a good amount of documentation for his versions 19 | which is still pretty well covers the current versions of MeshChat. 20 | The documentation can be found over at his blog, https://github.com/tpaskett/meshchat. 21 | 22 | ## MeshChat v2.0 - v2.10 23 | 24 | When AREDN firmware v3.22.6.0 was released in June 2022, the AREDN development 25 | team stopped including Perl in the distribution in favor of LUA. In preparation 26 | of this change Tim Wilkinson (KN6PLV) started rewriting MeshChat in LUA 27 | March 2022 with the first release of the new code base in April 2022. The 28 | new MeshChat code continued to receive bug fixes for a year. At which 29 | time Tim's involvement on the AREDN development team prevented him from 30 | continuing to maintain MeshChat. 31 | 32 | ## Future of MeshChat 33 | 34 | That brings the story upto the current time, September 2023, where I, 35 | Gerard Hickey (WT0F), have started to be the maintainer of the MeshChat 36 | code base. There has already been work to restructure the repository to 37 | make working with the code more effective and to automatically build 38 | packages when a release occurs. 39 | -------------------------------------------------------------------------------- /docs/Install.md: -------------------------------------------------------------------------------- 1 | # Installing MeshChat 2 | 3 | MeshChat is distributed as an Itsy package (IPK file) to be installed on an 4 | AREDN node. This is the simplest way to install MeshChat. 5 | 6 | Simply download the MeshChat package to your compute and then access the 7 | Administration panel in the AREDN's node setup. Under Package Management 8 | you will have the option to upload a package. Once uploaded the MeshChat 9 | system will be started within a couple of seconds. 10 | 11 | Usually there is not really any configuration that needs to be done, but 12 | review of the [configuration settings](../module/meshchatconfig.html) is 13 | suggested. 14 | 15 | Starting with `v2.12.0` the configuration of MeshChat occurs in the file 16 | `/www/cgi-bin/meshchat_local.lua`. Any configuration settings that need 17 | to be modified need to be entered into the `meshchat_local.lua` file and 18 | not in the `meshchatconfig.lua` file that previous version used to 19 | configure MeshChat. Making changes to `meshchatconfig.lua` will be lost 20 | when MeshChat is upgraded or downgraded. Making changes to the configuration 21 | still requires one to SSH into the node and edit `meshchat_local.lua` 22 | directly. 23 | 24 | ## Setting the MeshChat Zone Name 25 | 26 | MeshChat uses a zone name to locate other MeshChat servers running with 27 | the same zone name. Once servers are added to the same zone name, they 28 | will automatically synchronize their message databases with one another. 29 | Setting the zone name is done in the AREDN node adminstration settings 30 | under the advertised services. 31 | 32 | After a new install of MeshChat the installation will randomly generate 33 | a zone name and register it with the AREDN node. The service name 34 | (i.e. zone name) can be changed to the desired zone name used by other 35 | MeshChat servers. Once the service name has been saved, it is best to 36 | reboot the AREDN node to insure that MeshChat is restarted with the 37 | correct zone name. 38 | 39 | ## Installing MeshChat on Linux 40 | 41 | The current distribution of MeshChat does not currently support Linux. In 42 | order to run MeshChat on a Linux machine, one needs to download MeshChat 43 | v1.0.2 and install it on the Linux machine. Once installed, the configuration 44 | need to be updated to set the `api_host` setting to the hostname or IP 45 | of an AREDN node that has the MeshChat API package installed. 46 | -------------------------------------------------------------------------------- /docs/Troubleshooting.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | This is a "living" document. It is attempted to keep it up to date with 4 | any new problems and troubleshooting techniques. If you find something 5 | missing, please create an [Issue](https://github.com/hickey/meshchat/issues/new/choose) 6 | do describe what problem or issue is missing. Better yet is to fork the 7 | MeshChat repository, update the documentation in the forked repository 8 | and then generate a PR back to the official MeshChat repository. Your 9 | efforts will be greatly appreciated. 10 | 11 | It is important to realize that MeshChat is effectively two separate 12 | programs: one that runs in your browser (the frontend code) and one that 13 | runs on the AREDN node (the backend code or API). While it may not be 14 | obvious which piece of code is having the problem, it generally can be 15 | broken down as if there is an issue with the format of a message or it 16 | being displayed in the browser then the frontend code should be investigated. 17 | Otherwise the API should be investigated. 18 | 19 | ## Installation Issues 20 | 21 | There is a known issue that if an older AREDN firmware is being upgraded, 22 | any additional packages will need to be reinstalled after the node has 23 | completed the firmware upgrade. This should not be the case for AREDN 24 | firmware 3.23.8.0 or greater. 25 | 26 | If it appears that the installation of the package did not completely 27 | install or is not fully functional, check the node to determine how much 28 | disk space is available. Generally one should plan on a minimum of 100 KB 29 | of disk space for MeshChat to operate. 30 | 31 | Package installation failures also generally have an error message displayed 32 | above the upload button when there is a failure. This can help indicate 33 | what the failure type was, so it should be reported back as a project 34 | issue using the link above. 35 | 36 | ## Message Synchronization Issues 37 | 38 | In order for messages to be synchronized between MeshChat instances, the 39 | `meshchatsync` process needs to be running. Log into the node and execute 40 | `ps | grep meshchatsync` to see if the process exists. If it is not 41 | running, then one can start it with executing `/usr/local/bin/meshchatsync`. 42 | Doing so will keep the process attached to the current terminal and any 43 | error output will be displayed in the terminal. Once the terminal is 44 | exited, the `meshchatsync` process will terminate. So after determining 45 | that there are no errors being generated, it is best to reboot the node. 46 | This will allow `meshchatsync` to startup normally with no manual 47 | intervention. 48 | 49 | If it appears that `meshchatsync` is operating correctly, then the next 50 | item to check is that the message database exists and messages are being 51 | written to it. On an AREDN node, the message database is normally located 52 | in `/tmp/meshchat`. Check for a `messages.`. If the message 53 | database does exist, post a new message in the MeshChat instance on the 54 | node and insure that the message gets written to the message database. 55 | 56 | Also insure that the message database has write permissions on the file. 57 | 58 | -------------------------------------------------------------------------------- /luadox.conf: -------------------------------------------------------------------------------- 1 | [project] 2 | # Project name that is displayed on the top bar of each page 3 | name = MeshChat 4 | # HTML title that is appended to every page. If not defined, name is used. 5 | title = MeshChat (master) 6 | # A list of files or directories for LuaDox to parse. Globs are supported. 7 | # This can be spread across multiple lines if you want, as long as the 8 | # other lines are indented. 9 | files = meshchat* 10 | #files = /data/src/data/www/cgi-bin/meshchat.lua /data/src/data/www/cgi-bin/meshchatlib.lua 11 | # The directory containing the rendered output files, which will be created 12 | # if necessary. 13 | outdir = _site 14 | # Path to a custom css file that will be included on every page. This will 15 | # be copied into the outdir. 16 | # css = custom.css 17 | # Path to a custom favicon. This will be copied into the outdir. 18 | # favicon = img/favicon.png 19 | # If require()d files discovered in source should also be parsed. 20 | follow = false 21 | # Character encoding for input files, which defaults to the current system 22 | # locale. Output files are always utf8. 23 | encoding = utf8 24 | 25 | [manual] 26 | index = README.md 27 | history = docs/History.md 28 | install = docs/Install.md 29 | troubleshoot = docs/Troubleshooting.md 30 | -------------------------------------------------------------------------------- /meshchat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | --[[ 3 | 4 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks 5 | Copyright (C) 2022 Tim Wilkinson 6 | Base on code (C) Trevor Paskett (see https://github.com/tpaskett) 7 | See Contributors file for additional contributors 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation version 3 of the License. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | Additional Terms: 22 | 23 | Additional use restrictions exist on the AREDN(TM) trademark and logo. 24 | See AREDNLicense.txt for more info. 25 | 26 | Attributions to the AREDN Project must be retained in the source code. 27 | If importing this code into a new or existing project attribution 28 | to the AREDN project must be added to the source code. 29 | 30 | You must not misrepresent the origin of the material contained within. 31 | 32 | Modified versions must be modified to attribute to the original source 33 | and be marked in reasonable ways as differentiate it from the original 34 | version 35 | 36 | --]] 37 | 38 | package.path = package.path .. ";/www/cgi-bin/?.lua" 39 | 40 | require('luci.http') 41 | local json = require("luci.jsonc") 42 | require("nixio") 43 | require("meshchatconfig") 44 | require("meshchatlib") 45 | 46 | --- 47 | -- @module meshchat 48 | 49 | local query = {} 50 | local uploadfilename 51 | if os.getenv("QUERY_STRING") ~= "" or os.getenv("REQUEST_METHOD") == "POST" then 52 | local request = luci.http.Request(nixio.getenv(), 53 | function() 54 | local v = io.read(1024) 55 | if not v then 56 | io.close() 57 | end 58 | return v 59 | end 60 | ) 61 | local fp 62 | request:setfilehandler( 63 | function(meta, chunk, eof) 64 | if not fp then 65 | if meta and meta.file then 66 | uploadfilename = meta.file 67 | end 68 | nixio.fs.mkdir(tmp_upload_dir) 69 | fp = io.open(tmp_upload_dir .. "/file", "w") 70 | end 71 | if chunk then 72 | fp:write(chunk) 73 | end 74 | if eof then 75 | fp:close() 76 | end 77 | end 78 | ) 79 | query = request:formvalue() 80 | end 81 | 82 | --- Return an error page to a browser. 83 | -- @tparam string msg Error message to be displayed 84 | -- 85 | function error(msg) 86 | print("Content-type: text/plain\r") 87 | print("\r") 88 | print(msg) 89 | end 90 | 91 | --- @section API 92 | 93 | --- Returns a JSON document with basic node configuration. 94 | -- 95 | -- ## API Parameters 96 | -- | Parameter | Required | Description | 97 | -- |-----------|----------|------------------------------------------| 98 | -- | action | yes | Must be set to `config` | 99 | -- 100 | -- ## API Response 101 | -- 102 | -- @example 103 | -- { 104 | -- "version": "meshchat_version", 105 | -- "node": "node_name", 106 | -- "zone": "meshchat_zone_name" 107 | -- } 108 | -- 109 | function config() 110 | print("Content-type: application/json\r") 111 | print("\r") 112 | 113 | local settings = { 114 | version = app_version, 115 | protocol_verison = protocol_version, 116 | node = node_name(), 117 | zone = zone_name(), 118 | default_channel = default_channel, 119 | debug = debug, 120 | } 121 | 122 | print(json.stringify(settings)) 123 | end 124 | 125 | --- Send a message to the MeshChat instance. 126 | -- 127 | -- ## API Parameters 128 | -- | Parameter | Required | Description | 129 | -- |-----------|----------|------------------------------------------| 130 | -- | action | yes | Must be set to `send_message` | 131 | -- | message | yes | Message body | 132 | -- | call_sign | yes | Call sign of the sender | 133 | -- | channel | no | Channel name to post message | 134 | -- | epoch | no | Timestamp specified as unixtime | 135 | -- 136 | -- @note message 137 | -- Needs to have newslines and double quotes escaped. 138 | -- 139 | -- @note channel 140 | -- If not specified or set to empty string will post message to 141 | -- `Everything` channel 142 | -- 143 | -- @note epoch 144 | -- If not specified, the current time on the MeshChat server will 145 | -- be used. 146 | -- 147 | -- ## API Response 148 | -- 149 | -- On a successful entry of the message into the database a success JSON 150 | -- document will be returned. 151 | -- 152 | -- @example 153 | -- { 154 | -- "status": 200, 155 | -- "response": "OK" 156 | -- } 157 | -- 158 | function send_message() 159 | print("Content-type: application/json\r") 160 | print("\r") 161 | 162 | local message = query.message:gsub("\n", "\\n"):gsub('"', '\\"'):gsub("\t", " ") 163 | local id = query.id or hash(); 164 | local epoch = os.time() 165 | if tonumber(query.epoch) > epoch then 166 | epoch = query.epoch 167 | end 168 | 169 | get_lock() 170 | 171 | local f = io.open(messages_db_file, "a") 172 | if not f then 173 | release_lock() 174 | -- TODO return a proper error code on failure 175 | die("Cannot send message") 176 | end 177 | f:write(id .. "\t" .. epoch .. "\t" .. message .. "\t" .. query.call_sign .. "\t" .. node_name() .. "\t" .. platform .. "\t" .. query.channel .. "\n") 178 | f:close() 179 | 180 | sort_and_trim_db() 181 | save_messages_db_version() 182 | 183 | release_lock() 184 | 185 | print([[{"status":200, "response":"OK"}]]) 186 | end 187 | 188 | --- Return a list of message stored on the MeshChat instance. 189 | -- 190 | -- ## API Parameters 191 | -- | Parameter | Required | Description | 192 | -- |-----------|----------|------------------------------------------| 193 | -- | action | yes | Must be set to `messages` | 194 | -- | call_sign | no | Call sign of the requester | 195 | -- | epoch | no | Timestamp specified as unixtime | 196 | -- | id | no | Generated MeshChat ID | 197 | -- 198 | -- ## API Response 199 | -- 200 | -- @example 201 | -- { 202 | -- "id": "id_str", 203 | -- "epoch": epoch_time, 204 | -- "message": "message_text", 205 | -- "call_sign": "sending_call_sign", 206 | -- "node": "originating_node", 207 | -- "platform": "originating_node_platform", 208 | -- "channel": "channel" 209 | -- } 210 | -- 211 | function messages() 212 | 213 | print("Content-type: application/json\r") 214 | local output = io.stdout 215 | local encoding = os.getenv("HTTP_ACCEPT_ENCODING") 216 | if encoding and encoding:match("gzip") then 217 | print "Content-Encoding: gzip\r" 218 | output = io.popen("gzip", "w") 219 | end 220 | print("\r") 221 | io.flush() 222 | 223 | get_lock() 224 | 225 | local node = node_name() 226 | 227 | -- read in message DB and parse the contents 228 | local messages = {} 229 | for line in io.lines(messages_db_file) 230 | do 231 | local id, epoch, message, call_sign, node, platform, channel = line:match("^(%S+)\t(%S+)\t(.+)\t([^\t]+)\t(%S*)\t(%S+)\t(%S*)$") 232 | if epoch and #epoch > 0 then 233 | messages[#messages + 1] = { 234 | id = id, 235 | epoch = tonumber(epoch), 236 | message = message:gsub("\\n", "\n"):gsub('\\"', '"'), 237 | call_sign = call_sign, 238 | node = node, 239 | platform = platform, 240 | channel = channel 241 | } 242 | end 243 | end 244 | 245 | if tonumber(query.epoch) and query.call_sign then 246 | local users = {} 247 | 248 | -- read the users status file 249 | if nixio.fs.stat(local_users_status_file) then 250 | for line in io.lines(local_users_status_file) 251 | do 252 | local call_sign = line:match("^([^\t]+)\t") 253 | if call_sign then 254 | users[call_sign] = line 255 | end 256 | end 257 | end 258 | 259 | -- set the timestamp 260 | local epoch = os.time() 261 | if tonumber(query.epoch) > epoch then 262 | epoch = query.epoch 263 | end 264 | 265 | -- rewrite user status file updating the timestamp for requesting call sign 266 | -- query.id is the meshchat_id 267 | local f = io.open(local_users_status_file, "w") 268 | if f then 269 | local found_user = false 270 | for call_sign, line in pairs(users) 271 | do 272 | if call_sign == query.call_sign then 273 | f:write(call_sign .. "\t" .. query.id .. "\t" .. node .. "\t" .. epoch .. "\t" .. platform .. "\n") 274 | found_user = true 275 | else 276 | f:write(line .. "\n") 277 | end 278 | end 279 | if not found_user then 280 | f:write(query.call_sign .. "\t" .. query.id .. "\t" .. node .. "\t" .. epoch .. "\t" .. platform .. "\n") 281 | end 282 | f:close() 283 | end 284 | end 285 | 286 | release_lock() 287 | 288 | -- order messages according to time 289 | table.sort(messages, function(a, b) return a.epoch > b.epoch end) 290 | 291 | output:write(json.stringify(messages)) 292 | output:flush() 293 | 294 | end 295 | 296 | --- Return a JSON document describing the sync status. 297 | -- 298 | -- ## API Parameters 299 | -- | Parameter | Required | Description | 300 | -- |-----------|----------|------------------------------------------| 301 | -- | action | yes | Must be set to `sync_status` | 302 | -- 303 | -- ## API Response 304 | -- 305 | -- @example 306 | -- { 307 | -- "node": "node_name", 308 | -- "epoch": sync_time 309 | -- } 310 | -- 311 | function sync_status() 312 | print("Content-type: application/json\r") 313 | print("\r") 314 | 315 | get_lock() 316 | 317 | local status = {} 318 | if nixio.fs.stat(sync_status_file) then 319 | for line in io.lines(sync_status_file) 320 | do 321 | local node, epoch = line:match("^(.*)\t(.*)$") 322 | status[#status + 1] = { 323 | node = node, 324 | epoch = tonumber(epoch) 325 | } 326 | end 327 | end 328 | 329 | release_lock() 330 | 331 | table.sort(status, function(a, b) return a.epoch > b.epoch end) 332 | 333 | print(json.stringify(status)) 334 | end 335 | 336 | --- Return a list of messages as text. 337 | function messages_raw() 338 | get_lock() 339 | 340 | local md5 = file_md5(messages_db_file) 341 | local lines = {} 342 | for line in io.lines(messages_db_file) 343 | do 344 | lines[#lines + 1] = line 345 | end 346 | 347 | release_lock() 348 | 349 | print("Content-MD5: " .. md5 .. "\r") 350 | print("Content-type: text/plain\r") 351 | print("\r") 352 | 353 | for _, line in ipairs(lines) 354 | do 355 | print(line) 356 | end 357 | end 358 | 359 | --- Return the current MD5 has of the messages database. 360 | function messages_md5() 361 | get_lock() 362 | 363 | local md5 = file_md5(messages_db_file) 364 | 365 | release_lock() 366 | 367 | print("Content-type: text/plain\r") 368 | print("\r") 369 | print(md5) 370 | end 371 | 372 | --- Package the raw messages as the messages.txt file. 373 | function messages_download() 374 | get_lock() 375 | 376 | local md5 = file_md5(messages_db_file) 377 | local lines = {} 378 | for line in io.lines(messages_db_file) 379 | do 380 | lines[#lines + 1] = line 381 | end 382 | 383 | release_lock() 384 | 385 | print("Content-MD5: " .. md5 .. "\r") 386 | print("Content-Disposition: attachment; filename=messages.txt;\r") 387 | print("Content-type: text/plain\r") 388 | print("\r") 389 | 390 | for _, line in ipairs(lines) 391 | do 392 | print(line) 393 | end 394 | end 395 | 396 | --- Return the list of users as raw text. 397 | function users_raw() 398 | get_lock() 399 | 400 | local md5 = file_md5(local_users_status_file) 401 | local lines = {} 402 | for line in io.lines(local_users_status_file) 403 | do 404 | lines[#lines + 1] = line 405 | end 406 | 407 | release_lock() 408 | 409 | print("Content-MD5: " .. md5 .. "\r") 410 | print("Content-type: text/plain\r") 411 | print("\r") 412 | 413 | for _, line in ipairs(lines) 414 | do 415 | print(line) 416 | end 417 | end 418 | 419 | --- Return a JSON document describing the logged in users. 420 | -- 421 | -- ## API Parameters 422 | -- | Parameter | Required | Description | 423 | -- |-----------|----------|------------------------------------------| 424 | -- | action | yes | Must be set to `users` | 425 | -- 426 | -- ## API Response 427 | -- 428 | -- @example 429 | -- { 430 | -- "id": "id_str", 431 | -- "epoch": epoch_time, 432 | -- "call_sign": "sender_call_sign', 433 | -- "node": "originating_node", 434 | -- "platform": "originating_platform", 435 | -- } 436 | -- 437 | function users() 438 | print("Content-type: application/json\r") 439 | print("\r") 440 | 441 | get_lock() 442 | 443 | local users = {} 444 | for line in io.lines(local_users_status_file) 445 | do 446 | local call_sign, id, node, epoch, platform = line:match("^(.*)\t(.*)\t(.*)\t(.*)\t(.*)$") 447 | if epoch and #epoch > 0 then 448 | users[#users + 1] = { 449 | epoch = tonumber(epoch), 450 | id = id, 451 | call_sign = call_sign, 452 | node = node, 453 | platform = platform 454 | } 455 | end 456 | end 457 | for line in io.lines(remote_users_status_file) 458 | do 459 | local call_sign, id, node, epoch, platform = line:match("^(.*)\t(.*)\t(.*)\t(.*)\t(.*)$") 460 | if epoch and #epoch > 0 then 461 | users[#users + 1] = { 462 | epoch = tonumber(epoch), 463 | id = id, 464 | call_sign = call_sign, 465 | node = node, 466 | platform = platform 467 | } 468 | end 469 | end 470 | 471 | release_lock() 472 | 473 | table.sort(users, function(a, b) return a.epoch > b.epoch end) 474 | 475 | print(json.stringify(users)) 476 | end 477 | 478 | --- Return a list of files as plain text. 479 | function local_files_raw() 480 | get_lock() 481 | 482 | local tmp_file = meshchat_path .. "/meshchat_files_local." .. nixio.getpid() 483 | local f = io.open(tmp_file, "w") 484 | if not f then 485 | die("Cannot list local files") 486 | end 487 | local name = node_name() .. ":" .. os.getenv("SERVER_PORT") 488 | for file in nixio.fs.dir(local_files_dir) 489 | do 490 | local stat = nixio.fs.stat(local_files_dir .. "/" .. file) 491 | f:write(file .. "\t" .. name .. "\t" .. stat.size .. "\t" .. stat.mtime .. platform .. "\n") 492 | end 493 | f:close() 494 | 495 | local md5 = file_md5(tmp_file) 496 | 497 | release_lock() 498 | 499 | print("Content-MD5: " .. md5 .. "\r") 500 | print("Content-type: text/plain\r") 501 | print("\r") 502 | 503 | for line in io.lines(tmp_file) 504 | do 505 | print(line) 506 | end 507 | 508 | nixio.fs.remove(tmp_file) 509 | end 510 | 511 | --- Return a specified file as a download. 512 | -- 513 | -- ## API Parameters 514 | -- | Parameter | Required | Description | 515 | -- |-----------|----------|------------------------------------------| 516 | -- | action | yes | Must be set to `file_download` | 517 | -- | file | yes | Name of file to downlaod | 518 | -- 519 | -- ## API Response 520 | -- 521 | -- Returns a page as an octet-stream that is tagged as an attachment 522 | -- to cause the browser to receive the file as a download. 523 | -- 524 | function file_download() 525 | local file = query.file 526 | local file_path = local_files_dir .. "/" .. file 527 | 528 | if file == "" or not nixio.fs.stat(file_path) then 529 | error("no file") 530 | return 531 | end 532 | 533 | get_lock() 534 | 535 | local md5 = file_md5(file_path) 536 | local f = io.open(file_path, "rb") 537 | 538 | release_lock() 539 | 540 | print("Content-MD5: " .. md5 .. "\r") 541 | print("Content-Disposition: attachment; filename=\"" .. file .. "\";\r") 542 | print("Content-type: application/octet-stream\r") 543 | print("\r") 544 | 545 | if f then 546 | io.write(f:read("*a")) 547 | f:close() 548 | end 549 | end 550 | 551 | --- Return a JSON document describing the list of files. 552 | -- 553 | -- ## API Parameters 554 | -- | Parameter | Required | Description | 555 | -- |-----------|----------|------------------------------------------| 556 | -- | action | yes | Must be set to `files` | 557 | -- 558 | -- ## API Response 559 | -- 560 | -- @example 561 | -- { 562 | -- "file": "filename", 563 | -- "epoch": modification_time, 564 | -- "size": file_size_in_bytes, 565 | -- "node": "originating_node", 566 | -- "platform": "originating_platform" 567 | -- } 568 | -- 569 | function files() 570 | print("Content-type: application/json\r") 571 | print("\r") 572 | 573 | get_lock() 574 | 575 | local files = {} 576 | local node = node_name() .. ":" .. os.getenv("SERVER_PORT") 577 | for file in nixio.fs.dir(local_files_dir) 578 | do 579 | local stat = nixio.fs.stat(local_files_dir .. "/" .. file) 580 | files[#files + 1] = { 581 | file = file, 582 | epoch = stat.mtime, 583 | size = stat.size, 584 | node = node, 585 | platform = platform 586 | } 587 | files[#files]["local"] = 1 588 | end 589 | for file in nixio.fs.dir(meshchat_path) 590 | do 591 | if file:match("^remote_files%.") then 592 | for line in io.lines(meshchat_path .. "/" .. file) 593 | do 594 | local name, node, size, epoch, platform = line:match("^(.*)\t(.*)\t(.*)\t(.*)\t(.*)$") 595 | if epoch and #epoch > 0 then 596 | files[#files + 1] = { 597 | file = name, 598 | epoch = tonumber(epoch), 599 | size = size, 600 | node = node, 601 | platform = platform 602 | } 603 | files[#files]["local"] = 0 604 | end 605 | end 606 | end 607 | end 608 | 609 | local stats = file_storage_stats() 610 | 611 | release_lock() 612 | 613 | table.sort(files, function(a, b) return a.epoch > b.epoch end) 614 | 615 | print(json.stringify({ 616 | stats = stats, 617 | files = files 618 | })) 619 | end 620 | 621 | --- Delete the specified file. 622 | function delete_file() 623 | nixio.fs.remove(local_files_dir .. "/" .. query.file) 624 | print("Content-type: application/json\r") 625 | print("\r") 626 | print([[{"status":200, "response":"OK"}]]) 627 | end 628 | 629 | --- Return the current version string for the messages database. 630 | function messages_version() 631 | print("Content-type: text/plain\r") 632 | print("\r") 633 | print(get_messages_db_version()) 634 | end 635 | 636 | --- Return a JSON document of the messages database. 637 | function messages_version_ui() 638 | print("Content-type: application/json\r") 639 | print("\r") 640 | 641 | print(string.format([[{"messages_version":%s}]], get_messages_db_version())) 642 | 643 | get_lock() 644 | 645 | local users = {} 646 | for line in io.lines(local_users_status_file) 647 | do 648 | local call_sign = line:match("^([^\t]+)\t") 649 | if call_sign then 650 | users[call_sign] = line 651 | end 652 | end 653 | 654 | local node = node_name() 655 | local epoch = os.time() 656 | if tonumber(query.epoch) > epoch then 657 | epoch = query.epoch 658 | end 659 | 660 | -- TODO refactor here and messages function into a single code block 661 | local f = io.open(local_users_status_file, "w") 662 | if f then 663 | local found_user = false 664 | for call_sign, line in pairs(users) 665 | do 666 | if call_sign == query.call_sign then 667 | f:write(call_sign .. "\t" .. query.id .. "\t" .. node .. "\t" .. epoch .. "\t" .. platform .. "\n") 668 | found_user = true 669 | else 670 | f:write(line .. "\n") 671 | end 672 | end 673 | if not found_user then 674 | f:write(query.call_sign .. "\t" .. query.id .. "\t" .. node .. "\t" .. epoch .. "\t" .. platform .. "\n") 675 | end 676 | f:close() 677 | end 678 | 679 | release_lock() 680 | end 681 | 682 | --- Return a JSON document describing all the hosts. 683 | -- 684 | -- ## API Parameters 685 | -- | Parameter | Required | Description | 686 | -- |-----------|----------|------------------------------------------| 687 | -- | action | yes | Must be set to `hosts` | 688 | -- 689 | -- ## API Response 690 | -- 691 | -- @example 692 | -- { 693 | -- "ip": "ip_address", 694 | -- "hostname": "hostname", 695 | -- "node": "node_name" 696 | -- } 697 | -- 698 | function hosts() 699 | print("Content-type: application/json\r") 700 | print("\r") 701 | 702 | local node = node_name() 703 | local hosts = {} 704 | for line in io.lines("/var/dhcp.leases") 705 | do 706 | local epoch, mac1, ip, hostname, mac2 = line:match("^(%S+)%s(%S+)%s(%S+)%s(%S+)%s(%S+)$") 707 | hosts[#hosts + 1] = { 708 | ip = ip, 709 | hostname = hostname, 710 | node = node 711 | } 712 | end 713 | 714 | for line in io.lines("/etc/config.mesh/_setup.dhcp.dmz") 715 | do 716 | local mac, num, hostname = line:match("^(%S+)%s(%S+)%s(%S+)$") 717 | local ip = gethostbyname(hostname) 718 | hosts[#hosts + 1] = { 719 | ip = ip, 720 | hostname = hostname, 721 | node = node 722 | } 723 | end 724 | 725 | for _, remote_node in ipairs(node_list()) 726 | do 727 | local f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " http://" .. remote_node .. ":8080/cgi-bin/meshchat?action=hosts_raw 2> /dev/null") 728 | if f then 729 | for line in f:lines() 730 | do 731 | if line ~= "" and not line:match("error") then 732 | local ip, hostname = line:match("^(.+)\t(.+)$") 733 | hosts[#hosts + 1] = { 734 | ip = ip, 735 | hostname = hostname, 736 | node = remote_node 737 | } 738 | end 739 | end 740 | f:close() 741 | end 742 | end 743 | 744 | table.sort(hosts, function(a, b) return a.hostname < b.hostname end) 745 | 746 | print(json.stringify(hosts)) 747 | end 748 | 749 | --- Return a list of hosts as plain text. 750 | function hosts_raw() 751 | print("Content-type: application/json\r") 752 | print("\r") 753 | 754 | local hosts = {} 755 | for line in io.lines("/var/dhcp.leases") 756 | do 757 | local epoch, mac1, ip, hostname, mac2 = line:match("^(%S+)%s(%S+)%s(%S+)%s(%S+)%s(%S+)$") 758 | hosts[#hosts + 1] = { 759 | ip = ip, 760 | hostname = hostname 761 | } 762 | end 763 | 764 | for line in io.lines("/etc/config.mesh/_setup.dhcp.dmz") 765 | do 766 | local mac, num, hostname = line:match("^(%S+)%s(%S+)%s(%S+)$") 767 | local ip = gethostbyname(hostname) 768 | hosts[#hosts + 1] = { 769 | ip = ip, 770 | hostname = hostname 771 | } 772 | end 773 | 774 | for _, host in ipairs(hosts) 775 | do 776 | print(host.ip .. "\t" .. host.hostname) 777 | end 778 | end 779 | 780 | --- Store a file into the file directory. 781 | function upload_file() 782 | local new_file_size = nixio.fs.stat(tmp_upload_dir .. "/file").size 783 | 784 | get_lock() 785 | 786 | local stats = file_storage_stats() 787 | 788 | release_lock() 789 | 790 | print("Content-type: application/json\r") 791 | print("\r") 792 | 793 | if new_file_size > stats.files_free then 794 | nixio.fs.remove(tmp_upload_dir .. "/file") 795 | print([[{"status":500, "response":"Not enough storage, delete some files"}]]) 796 | else 797 | local fi = io.open(tmp_upload_dir .. "/file", "r") 798 | local fo = io.open(local_files_dir .. "/" .. uploadfilename, "w") 799 | fo:write(fi:read("*a")) 800 | fi:close() 801 | fo:close() 802 | nixio.fs.remove(tmp_upload_dir .. "/file") 803 | print([[{"status":200, "response":"OK"}]]) 804 | end 805 | end 806 | 807 | --- Return a list of nodes running MeshChat as text. 808 | -- 809 | -- ## API Parameters 810 | -- | Parameter | Required | Description | 811 | -- |-----------|----------|------------------------------------------| 812 | -- | action | yes | Must be set to `meshchat_nodes` | 813 | -- | zone_name | yes | MeshChat zone name | 814 | -- 815 | -- ## API Response 816 | -- 817 | -- The list of nodes and ports seperated by a tab. 818 | -- 819 | -- @example 820 | -- node1 8080 821 | -- node2 8080 822 | -- 823 | function meshchat_nodes() 824 | print("Content-type: text/plain\r") 825 | print("\r") 826 | 827 | local pattern = "http://(%S+):(%d+)/meshchat|tcp|" .. str_escape(query.zone_name) .. "%s" 828 | for line in io.lines("/var/run/services_olsr") 829 | do 830 | local node, port = line:match(pattern) 831 | if node and port then 832 | print(node .. "\t" .. port) 833 | end 834 | end 835 | end 836 | 837 | --- Return a JSON document of the action log. 838 | -- 839 | -- Currently this call returns an empty list. In the future it will 840 | -- return a list of action log events. 841 | -- 842 | function action_log() 843 | print("Content-type: application/json\r") 844 | print("\r") 845 | print("[]") 846 | end 847 | 848 | -- Command dispatch -- 849 | 850 | if query.action == "messages" then 851 | messages() 852 | elseif query.action == "config" then 853 | config() 854 | elseif query.action == "send_message" then 855 | send_message() 856 | elseif query.action == "sync_status" then 857 | sync_status() 858 | elseif query.action == "messages_raw" then 859 | messages_raw() 860 | elseif query.action == "messages_md5" then 861 | messages_md5() 862 | elseif query.action == "messages_download" then 863 | messages_download() 864 | elseif query.action == "users_raw" then 865 | users_raw() 866 | elseif query.action == "users" then 867 | users() 868 | elseif query.action == "local_files_raw" then 869 | local_files_raw() 870 | elseif query.action == "file_download" then 871 | file_download() 872 | elseif query.action == "files" then 873 | files() 874 | elseif query.action == "delete_file" then 875 | delete_file() 876 | elseif query.action == "messages_version" then 877 | messages_version() 878 | elseif query.action == "messages_version_ui" then 879 | messages_version_ui() 880 | elseif query.action == "hosts" then 881 | hosts() 882 | elseif query.action == "hosts_raw" then 883 | hosts_raw() 884 | elseif query.action == "upload_file" then 885 | upload_file() 886 | elseif query.action == "meshchat_nodes" then 887 | meshchat_nodes() 888 | elseif query.action == "action_log" then 889 | action_log() 890 | else 891 | error("error no action") 892 | end 893 | -------------------------------------------------------------------------------- /meshchatconfig.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | STOP STOP STOP DO NOT EDIT THIS FILE 3 | 4 | This file is used to set default values for MeshChat and should NOT 5 | be edited. Edits made to this file WILL BE LOST when upgrading or 6 | downgrading MeshChat. All the values below can be overridden by 7 | adding them to the meshchat_local.lua file located in the same 8 | directory as this file. 9 | 10 | EDIT THIS FILE AT YOUR PERIL. YOU HAVE BEEN WARNED. 11 | 12 | 13 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks 14 | Copyright (C) 2022 Tim Wilkinson 15 | Base on code (C) Trevor Paskett (see https://github.com/tpaskett) 16 | See Contributors file for additional contributors 17 | 18 | This program is free software: you can redistribute it and/or modify 19 | it under the terms of the GNU General Public License as published by 20 | the Free Software Foundation version 3 of the License. 21 | 22 | This program is distributed in the hope that it will be useful, 23 | but WITHOUT ANY WARRANTY; without even the implied warranty of 24 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 25 | GNU General Public License for more details. 26 | 27 | You should have received a copy of the GNU General Public License 28 | along with this program. If not, see . 29 | 30 | Additional Terms: 31 | 32 | Additional use restrictions exist on the AREDN(TM) trademark and logo. 33 | See AREDNLicense.txt for more info. 34 | 35 | Attributions to the AREDN Project must be retained in the source code. 36 | If importing this code into a new or existing project attribution 37 | to the AREDN project must be added to the source code. 38 | 39 | You must not misrepresent the origin of the material contained within. 40 | 41 | Modified versions must be modified to attribute to the original source 42 | and be marked in reasonable ways as differentiate it from the original 43 | version 44 | 45 | --]] 46 | 47 | --- 48 | -- @module meshchatconfig 49 | -- @section MeshChat Configuration 50 | 51 | --- Base directory to store all MeshChat generated files 52 | -- @type string 53 | meshchat_path = "/tmp/meshchat" 54 | --- Maximum number of messages in the database 55 | -- @type int 56 | max_messages_db_size = 500 57 | --- Maximum amount of filesystem space for storing files 58 | -- @type int 59 | max_file_storage = 512 * 1024 60 | lock_file = meshchat_path .. "/lock" 61 | messages_db_file = meshchat_path .. "/messages" 62 | messages_db_file_orig = meshchat_path .. "/messages" 63 | sync_status_file = meshchat_path .. "/sync_status" 64 | local_users_status_file = meshchat_path .. "/users_local" 65 | remote_users_status_file = meshchat_path .. "/users_remote" 66 | remote_files_file = meshchat_path .. "/files_remote" 67 | messages_version_file = meshchat_path .. "/messages_version" 68 | local_files_dir = meshchat_path .. "/files" 69 | tmp_upload_dir = "/tmp/web/upload" 70 | --- How often to check for new messages 71 | -- @type int 72 | poll_interval = 10 73 | non_meshchat_poll_interval = 600 74 | valid_future_message_time = 30 * 24 * 60 * 60 75 | connect_timeout = 5 76 | speed_time = 10 77 | speed_limit = 1000 78 | --- Type of node that MeshChat is installed on ("node" or "pi") 79 | -- @type string 80 | platform = "node" 81 | --- Turn debug message on 82 | -- @type bool 83 | debug = 0 84 | extra_nodes = {} 85 | --- MeshChat protocol version 86 | -- @type string 87 | protocol_version = "1.02" 88 | app_version = "master" 89 | default_channel = "" 90 | 91 | require("meshchat_local") 92 | -------------------------------------------------------------------------------- /meshchatlib.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks 4 | Copyright (C) 2022 Tim Wilkinson 5 | Base on code (C) Trevor Paskett (see https://github.com/tpaskett) 6 | See Contributors file for additional contributors 7 | 8 | This program is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation version 3 of the License. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | Additional Terms: 21 | 22 | Additional use restrictions exist on the AREDN(TM) trademark and logo. 23 | See AREDNLicense.txt for more info. 24 | 25 | Attributions to the AREDN Project must be retained in the source code. 26 | If importing this code into a new or existing project attribution 27 | to the AREDN project must be added to the source code. 28 | 29 | You must not misrepresent the origin of the material contained within. 30 | 31 | Modified versions must be modified to attribute to the original source 32 | and be marked in reasonable ways as differentiate it from the original 33 | version 34 | 35 | --]] 36 | 37 | require("nixio") 38 | require("uci") 39 | 40 | --- @module meshchatlib 41 | 42 | --- Exit the program with an error message. 43 | -- 44 | -- @tparam string msg Message to display 45 | -- 46 | function die(msg) 47 | os.exit(-1) 48 | end 49 | 50 | --- Execute a command and capture the output. 51 | -- 52 | -- @tparam string cmd Command line to execute 53 | -- @treturn string stdout of the command 54 | -- 55 | function capture(cmd) 56 | local f = io.popen(cmd) 57 | if not f then 58 | return "" 59 | end 60 | local output = f:read("*a") 61 | f:close() 62 | return output 63 | end 64 | 65 | --- 66 | -- Retrieve the current node name. 67 | -- 68 | -- This function will interogate the UCI settings to retrieve the current 69 | -- node name stored in the `hsmmmesh` settings. 70 | -- 71 | -- @treturn string Name of current node 72 | -- 73 | function node_name() 74 | return uci.cursor("/etc/local/uci"):get("hsmmmesh", "settings", "node") or "" 75 | end 76 | 77 | --- 78 | -- Retrieve the current MeshChat zone name that the node is operating under. 79 | -- 80 | -- @treturn string Name of MeshChat zone 81 | -- 82 | function zone_name() 83 | local dmz_mode = uci.cursor("/etc/config.mesh"):get("aredn", "@dmz[0]", "mode") 84 | local servfile = "/etc/config.mesh/_setup.services.nat" 85 | -- LAN mode is not set to NAT 86 | if dmz_mode ~= "0" then 87 | servfile = "/etc/config.mesh/_setup.services.dmz" 88 | end 89 | if nixio.fs.access(servfile) then 90 | for line in io.lines(servfile) 91 | do 92 | -- this will match the new service names with the icon metadata 93 | -- in this case we are using a space or a pipe to terminate 94 | -- the service name 95 | local zone = line:match("^(.-)[%s%|].*|meshchat$") 96 | if zone then 97 | return zone 98 | end 99 | end 100 | end 101 | return "MeshChat" 102 | end 103 | 104 | messages_db_file = messages_db_file_orig .. "." .. zone_name() 105 | 106 | local lock_fd 107 | function get_lock() 108 | if not lock_fd then 109 | lock_fd = nixio.open(lock_file, "w", "666") 110 | end 111 | lock_fd:lock("lock") 112 | end 113 | 114 | function release_lock() 115 | lock_fd:lock("ulock") 116 | end 117 | 118 | --- Generate the MD5 sum of a file. 119 | -- 120 | -- This under the covers relies on `md5sum` and executes `md5sum` against 121 | -- the specified file. 122 | -- 123 | -- @note 124 | -- There is no checking to determine if `md5sum` is installed or 125 | -- executable. In the future, this may change. 126 | -- 127 | -- @tparam string file Path to file 128 | -- @treturn string Result of `md5sum` of the file 129 | -- 130 | function file_md5(file) 131 | if not nixio.fs.stat(file) then 132 | return "" 133 | end 134 | local output = capture("md5sum " .. file:gsub(" ", "\\ ")):match("^(%S+)%s") 135 | return output and output or "" 136 | end 137 | 138 | function get_messages_db_version() 139 | for line in io.lines(messages_version_file) 140 | do 141 | line = line:gsub("\n$", "") 142 | return line 143 | end 144 | end 145 | 146 | function save_messages_db_version() 147 | local f = io.open(messages_version_file, "w") 148 | f:write(get_messages_version_file() .. "\n") 149 | f:close() 150 | nixio.fs.chmod(messages_version_file, "666") 151 | end 152 | 153 | function get_messages_version_file() 154 | local sum = 0 155 | for line in io.lines(messages_db_file) 156 | do 157 | local key = line:match("^([0-9a-f]+)") 158 | if key then 159 | sum = sum + tonumber(key, 16) 160 | end 161 | end 162 | return sum 163 | end 164 | 165 | --- Generate a unique hash. 166 | -- 167 | -- Combine the current time (epoch time) and a randomly generated number 168 | -- between 0 - 99999 and run through `md5sum` to generate a random hash. 169 | -- 170 | -- @note 171 | -- There is no checking to determine if `md5sum` is installed or 172 | -- executable. In the future, this may change. 173 | -- 174 | -- @treturn string Generated hash value 175 | -- 176 | function hash() 177 | return capture("echo " .. os.time() .. math.random(99999) .. " | md5sum"):sub(1, 8) 178 | end 179 | 180 | function sort_and_trim_db() 181 | local valid_time = os.time() + valid_future_message_time 182 | local unused_count = max_messages_db_size 183 | local messages = {} 184 | for line in io.lines(messages_db_file) 185 | do 186 | local id, epoch = line:match("^(%x+)\t(%S+)\t") 187 | -- ignore messages that are too far in the future (assume they're errors) 188 | epoch = tonumber(epoch) 189 | if epoch and epoch < valid_time then 190 | messages[#messages + 1] = { 191 | epoch = epoch, 192 | id = tonumber(id, 16), 193 | line = line 194 | } 195 | end 196 | unused_count = unused_count - 1 197 | end 198 | 199 | table.sort(messages, function(a, b) 200 | if a.epoch == b.epoch then 201 | return a.id < b.id 202 | else 203 | return a.epoch < b.epoch 204 | end 205 | end) 206 | 207 | local f = io.open(messages_db_file, "w") 208 | for _, line in ipairs(messages) 209 | do 210 | unused_count = unused_count + 1 211 | if unused_count > 0 then 212 | f:write(line.line .. "\n") 213 | end 214 | end 215 | f:close() 216 | end 217 | 218 | function file_storage_stats() 219 | local lines = capture("df -k " .. local_files_dir) 220 | local blocks, used, available, perc = lines:match("(%d+)%s+(%d+)%s+(%d+)%s+(%d+)%%") 221 | used = tonumber(used) * 1024 222 | available = tonumber(available) * 1024 223 | local total = used + available 224 | 225 | local local_files_bytes = 0 226 | for file in nixio.fs.dir(local_files_dir) 227 | do 228 | local_files_bytes = local_files_bytes + nixio.fs.stat(local_files_dir .. "/" .. file).size 229 | end 230 | 231 | if max_file_storage - local_files_bytes < 0 then 232 | local_files_bytes = max_file_storage 233 | end 234 | 235 | return { 236 | total = total, 237 | used = used, 238 | files = local_files_bytes, 239 | files_free = max_file_storage - local_files_bytes, 240 | allowed = max_file_storage 241 | } 242 | end 243 | 244 | function gethostbyname(hostname) 245 | return capture("nslookup " .. hostname):match("Address 1:%s*([%d%.]+)") 246 | end 247 | 248 | function node_list() 249 | if not nixio.fs.stat("/var/run/services_olsr") then 250 | return {} 251 | end 252 | local local_node = node_name():lower() 253 | local zone = zone_name() 254 | 255 | local nodes = {} 256 | local pattern = "http://(%S+):(%d+)/meshchat|tcp|" .. str_escape(zone) .. "%s" 257 | for line in io.lines("/var/run/services_olsr") 258 | do 259 | local node, port = line:match(pattern) 260 | if node and port then 261 | node = node:lower() 262 | if node ~= local_node then 263 | nodes[#nodes + 1] = { 264 | platform = (port == "8080" and "node" or "pi"), 265 | node = node, 266 | port = port 267 | } 268 | end 269 | end 270 | end 271 | 272 | for _, extra in ipairs(extra_nodes) 273 | do 274 | nodes[#node + 1] = extra 275 | end 276 | 277 | return nodes 278 | end 279 | 280 | --- 281 | -- Escape percent signs. 282 | -- 283 | -- @tparam string str String to encode 284 | -- @treturn string Encoded string 285 | -- 286 | function str_escape(str) 287 | return str:gsub("%(", "%%("):gsub("%)", "%%)"):gsub("%%", "%%%%"):gsub("%.", "%%."):gsub("%+", "%%+"):gsub("-", "%%-"):gsub("%*", "%%*"):gsub("%[", "%%["):gsub("%?", "%%?"):gsub("%^", "%%^"):gsub("%$", "%%$") 288 | end 289 | -------------------------------------------------------------------------------- /package/ipk-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ipkg-build -- construct a .ipk from a directory 3 | # Carl Worth 4 | # based on a script by Steve Redler IV, steve@sr-tech.com 5-21-2001 5 | set -e 6 | 7 | ipkg_extract_value() { 8 | sed -e "s/^[^:]*:[[:space:]]*//" 9 | } 10 | 11 | required_field() { 12 | field=$1 13 | 14 | value=`grep "^$field:" < $CONTROL/control | ipkg_extract_value` 15 | if [ -z "$value" ]; then 16 | echo "ipkg-build: Error: $CONTROL/control is missing field $field" ; 17 | PKG_ERROR=1 18 | fi 19 | echo $value 20 | } 21 | 22 | pkg_appears_sane() { 23 | local pkg_dir=$1 24 | 25 | local owd=`pwd` 26 | cd $pkg_dir 27 | 28 | PKG_ERROR=0 29 | if [ ! -f "$CONTROL/control" ]; then 30 | echo "ipkg-build: Error: Control file $pkg_dir/$CONTROL/control not found." 31 | cd $owd 32 | return 1 33 | fi 34 | 35 | pkg=`required_field Package` 36 | version=`required_field Version` 37 | arch=`required_field Architecture` 38 | required_field Maintainer >/dev/null 39 | required_field Description >/dev/null 40 | 41 | if echo $pkg | grep '[^a-z0-9.+-]'; then 42 | echo "ipkg-build: Error: Package name $name contains illegal characters, (other than [a-z0-9.+-])" 43 | PKG_ERROR=1; 44 | fi 45 | 46 | local bad_fields=`sed -ne 's/^\([^[:space:]][^:[:space:]]\+[[:space:]]\+\)[^:].*/\1/p' < $CONTROL/control | sed -e 's/\\n//'` 47 | if [ -n "$bad_fields" ]; then 48 | bad_fields=`echo $bad_fields` 49 | echo "ipkg-build: Error: The following fields in $CONTROL/control are missing a ':'" 50 | echo " $bad_fields" 51 | echo "ipkg-build: This may be due to a missing initial space for a multi-line field value" 52 | PKG_ERROR=1 53 | fi 54 | 55 | for script in $CONTROL/preinst $CONTROL/postinst $CONTROL/prerm $CONTROL/postrm; do 56 | if [ -f $script -a ! -x $script ]; then 57 | echo "ipkg-build: Error: package script $script is not executable" 58 | PKG_ERROR=1 59 | fi 60 | done 61 | 62 | if [ -f $CONTROL/conffiles ]; then 63 | for cf in `cat $CONTROL/conffiles`; do 64 | if [ ! -f ./$cf ]; then 65 | echo "ipkg-build: Error: $CONTROL/conffiles mentions conffile $cf which does not exist" 66 | PKG_ERROR=1 67 | fi 68 | done 69 | fi 70 | 71 | cd $owd 72 | return $PKG_ERROR 73 | } 74 | 75 | ### 76 | # ipkg-build "main" 77 | ### 78 | 79 | case $# in 80 | 1) 81 | dest_dir=. 82 | ;; 83 | 2) 84 | dest_dir=$2 85 | ;; 86 | *) 87 | echo "Usage: ipkg-build []" ; 88 | exit 1 89 | ;; 90 | esac 91 | 92 | pkg_dir=$1 93 | 94 | if [ ! -d $pkg_dir ]; then 95 | echo "ipkg-build: Error: Directory $pkg_dir does not exist" 96 | exit 1 97 | fi 98 | 99 | # CONTROL is second so that it takes precedence 100 | CONTROL= 101 | [ -d $pkg_dir/DEBIAN ] && CONTROL=DEBIAN 102 | [ -d $pkg_dir/CONTROL ] && CONTROL=CONTROL 103 | if [ -z "$CONTROL" ]; then 104 | echo "ipkg-build: Error: Directory $pkg_dir has no CONTROL subdirectory." 105 | exit 1 106 | fi 107 | 108 | if ! pkg_appears_sane $pkg_dir; then 109 | echo "Please fix the above errors and try again." 110 | exit 1 111 | fi 112 | 113 | tmp_dir=$dest_dir/IPKG_BUILD.$$ 114 | mkdir $tmp_dir 115 | 116 | tar -C $pkg_dir --exclude=$CONTROL -czf $tmp_dir/data.tar.gz . 117 | tar -C $pkg_dir/$CONTROL -czf $tmp_dir/control.tar.gz . 118 | 119 | echo "2.0" > $tmp_dir/debian-binary 120 | 121 | pkg_file=$dest_dir/${pkg}_${version}_${arch}.ipk 122 | tar -C $tmp_dir -czf $pkg_file debian-binary data.tar.gz control.tar.gz 123 | rm $tmp_dir/debian-binary $tmp_dir/data.tar.gz $tmp_dir/control.tar.gz 124 | rmdir $tmp_dir 125 | 126 | echo "Packaged contents of $pkg_dir into $pkg_file" 127 | -------------------------------------------------------------------------------- /package/meshchat-api/control: -------------------------------------------------------------------------------- 1 | Package: meshchat-api 2 | Version: 3 | Depends: lua 4 | Provides: 5 | Source: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY 6 | Section: net 7 | Priority: optional 8 | Maintainer: Tim Wilkinson (KN6PLV) and Trevor Paskett (K7FPV) 9 | Architecture: all 10 | Description: P2P distributed chat for mesh networks 11 | -------------------------------------------------------------------------------- /package/meshchat/control: -------------------------------------------------------------------------------- 1 | Package: meshchat 2 | Version: 3 | Depends: curl lua 4 | Provides: 5 | Source: package/meshchat 6 | Section: net 7 | Priority: optional 8 | Maintainer: Tim Wilkinson (KN6PLV) and Trevor Paskett (K7FPV) 9 | Architecture: all 10 | Description: P2P distributed chat for mesh networks 11 | -------------------------------------------------------------------------------- /package/meshchat/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | grep "|8080|meshchat" /etc/config.mesh/_setup.services.dmz &> /dev/null 4 | DMZPRESENT=$? 5 | grep "|8080|meshchat" /etc/config.mesh/_setup.services.nat &> /dev/null 6 | NATPRESENT=$? 7 | NODEMODE=$(uci -q -c /etc/local/uci/ get hsmmmesh.settings.config) 8 | RAND=$(awk 'BEGIN{srand();print int(rand()*10000) }') 9 | RESTART=0 10 | 11 | if [ "$DMZPRESENT" != 0 ]; then 12 | echo "MeshChat-$RAND|1|http|$(uname -n)|8080|meshchat" >> /etc/config.mesh/_setup.services.dmz 13 | RESTART=1 14 | fi 15 | 16 | if [ "$NATPRESENT" != 0 ]; then 17 | echo "MeshChat-$RAND|1|http|$(uname -n)|8080|meshchat" >> /etc/config.mesh/_setup.services.nat 18 | RESTART=1 19 | fi 20 | 21 | if [ "$NODEMODE" = "mesh" -a "$RESTART" = "1" ]; then 22 | echo "Applying service announcement" 23 | /usr/local/bin/node-setup -a -p mesh &> /dev/null 24 | /etc/init.d/olsrd restart &> /dev/null 25 | fi 26 | 27 | /etc/init.d/meshchatsync enable 28 | /etc/init.d/meshchatsync start 29 | 30 | echo "
" 31 | 32 | echo "Mesh Chat has been setup at http://$(uname -n):8080/meshchat" 33 | echo "
" 34 | if [ "$RESTART" = "1" ]; then 35 | echo "An advertised service has been added for Mesh Chat on the Services configuration page" 36 | fi 37 | 38 | exit 0 39 | -------------------------------------------------------------------------------- /package/meshchat/preinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /etc/init.d/meshchatsync stop > /dev/null 2> /dev/null 4 | 5 | mkdir -p /www/meshchat 6 | 7 | # if there is not a meshchat_local.lua, then prepare one 8 | if [ ! -f /www/cgi-bin/meshchat_local.lua ]; then 9 | if [ -f /www/cgi-bin/meshchatconfig.lua ]; then 10 | cp /www/cgi-bin/meshchatconfig.lua /www/cgi-bin/meshchat_local.lua 11 | 12 | # remove vars that should not be in meshchat_local.lua 13 | sed -i "/^protocol_version/d; /^app_version/d" /www/cgi-bin/meshchat_local.lua 14 | else 15 | touch /www/cgi-bin/meshchat_local.lua 16 | fi 17 | fi 18 | 19 | exit 0 20 | -------------------------------------------------------------------------------- /package/meshchat/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | /etc/init.d/meshchatsync disable 4 | /etc/init.d/meshchatsync stop 5 | 6 | rm -rf /tmp/meshchat 7 | 8 | exit 0 9 | -------------------------------------------------------------------------------- /package/populate-meshchat-api-fs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script runs from the top of the project directory and 4 | # creates the filesystem image for the MeshChat API package. 5 | 6 | IPK_DIR=$1 7 | 8 | # Populate the CONTROL portion of the package 9 | mkdir -p $IPK_DIR/CONTROL 10 | cp -p package/meshchat-api/* $IPK_DIR/CONTROL/ 11 | sed -i "s%\$GITHUB_SERVER_URL%$GITHUB_SERVER_URL%" $IPK_DIR/CONTROL/control 12 | sed -i "s%\$GITHUB_REPOSITORY%$GITHUB_REPOSITORY%" $IPK_DIR/CONTROL/control 13 | 14 | # Populate the filesystem image for the package 15 | install -D api/meshchat -m 755 $IPK_DIR/www/cgi-bin/meshchat 16 | -------------------------------------------------------------------------------- /package/populate-meshchat-fs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script runs from the top of the project directory and 4 | # creates the filesystem image for the MeshChat API package. 5 | 6 | IPK_DIR=$1 7 | 8 | # Populate the CONTROL portion of the package 9 | mkdir -p $IPK_DIR/CONTROL 10 | cp -p package/meshchat/* $IPK_DIR/CONTROL/ 11 | sed -i "s%\$GITHUB_SERVER_URL%$GITHUB_SERVER_URL%" $IPK_DIR/CONTROL/control 12 | sed -i "s%\$GITHUB_REPOSITORY%$GITHUB_REPOSITORY%" $IPK_DIR/CONTROL/control 13 | 14 | # Populate the filesystem image for the package 15 | install -d $IPK_DIR/www/meshchat 16 | install www/* $IPK_DIR/www/meshchat 17 | install -d $IPK_DIR/www/cgi-bin 18 | install -m 755 meshchat $IPK_DIR/www/cgi-bin 19 | install -m 644 meshchatlib.lua $IPK_DIR/www/cgi-bin 20 | install -m 644 meshchatconfig.lua $IPK_DIR/www/cgi-bin 21 | install -D support/meshchatsync-init.d -m 755 $IPK_DIR/etc/init.d/meshchatsync 22 | install -D support/meshchatsync -m 755 $IPK_DIR/usr/local/bin/meshchatsync 23 | -------------------------------------------------------------------------------- /package/update-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This script runs from the top of the project directory and 4 | # updates the version number in the control file for the package 5 | # being built. 6 | 7 | IPK_DIR=$1 8 | 9 | if [[ -f VERSION ]]; then 10 | version=$(cat VERSION) 11 | else 12 | if [[ "${GITHUB_REF_TYPE}" == 'tag' ]]; then 13 | # ideally should only get version tags (i.e. 'v' followed by a number) 14 | if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9].* ]]; then 15 | version="${GITHUB_REF_NAME#v}" 16 | fi 17 | elif [[ -n "${CI_COMMIT_TAG}" ]]; then 18 | # ideally should only get version tags (i.e. 'v' followed by a number) 19 | if [[ "${CI_COMMIT_TAG}" =~ ^v[0-9].* ]]; then 20 | version="${CI_COMMIT_TAG#v}" 21 | fi 22 | else 23 | # branch gets date code-branch_name-commit 24 | date=$(date +%Y%m%d) 25 | branch=$(git rev-parse --abbrev-ref HEAD) 26 | # maybe a detached head, so check common vars for branch name 27 | if [[ -n "${CI_COMMIT_REF_NAME}" ]]; then 28 | branch="${CI_COMMIT_REF_NAME}" 29 | elif [[ -n "${GITHUB_REF_NAME}" ]]; then 30 | branch="${GITHUB_REF_NAME}" 31 | fi 32 | commit=$(git rev-parse --short HEAD) 33 | version="${date}-${branch}-${commit}" 34 | fi 35 | fi 36 | 37 | # write the version to a VERSION file 38 | echo "${version}" > VERSION 39 | echo "Updating code references to version ${version}" 40 | 41 | sed -i "s/^Version:.*/Version: $version/" $IPK_DIR/CONTROL/control 42 | 43 | # Update the version in meshchatconfig.lua if present 44 | if [[ -f $IPK_DIR/www/cgi-bin/meshchatconfig.lua ]]; then 45 | sed -i "s/^app_version.*$/app_version = \"${version}\"/" $IPK_DIR/www/cgi-bin/meshchatconfig.lua 46 | fi 47 | -------------------------------------------------------------------------------- /support/meshchatsync: -------------------------------------------------------------------------------- 1 | #!/usr/bin/lua 2 | --[[ 3 | 4 | Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks 5 | Copyright (C) 2022 Tim Wilkinson 6 | Based on code (C) Trevor Paskett (see https://github.com/tpaskett) 7 | See Contributors file for additional contributors 8 | 9 | This program is free software: you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation version 3 of the License. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | 21 | Additional Terms: 22 | 23 | Additional use restrictions exist on the AREDN(TM) trademark and logo. 24 | See AREDNLicense.txt for more info. 25 | 26 | Attributions to the AREDN Project must be retained in the source code. 27 | If importing this code into a new or existing project attribution 28 | to the AREDN project must be added to the source code. 29 | 30 | You must not misrepresent the origin of the material contained within. 31 | 32 | Modified versions must be modified to attribute to the original source 33 | and be marked in reasonable ways as differentiate it from the original 34 | version 35 | 36 | --]] 37 | 38 | package.path = package.path .. ";/www/cgi-bin/?.lua" 39 | require("nixio") 40 | require("meshchatconfig") 41 | require("meshchatlib") 42 | 43 | local sync_status = {} 44 | local non_mesh_chat_nodes = {} 45 | 46 | local node = node_name() 47 | 48 | if not nixio.fs.stat(meshchat_path) then 49 | nixio.fs.mkdir(meshchat_path) 50 | nixio.fs.mkdir(local_files_dir) 51 | end 52 | 53 | if not nixio.fs.stat(messages_db_file) then 54 | io.open(messages_db_file, "w"):close() 55 | nixio.fs.chmod(messages_db_file, "666") 56 | end 57 | 58 | io.open(local_users_status_file, "a"):close() 59 | io.open(remote_users_status_file, "a"):close() 60 | 61 | save_messages_db_version() 62 | 63 | nixio.fs.chmod(meshchat_path, "666") 64 | 65 | io.open(lock_file, "a"):close() 66 | 67 | function log_status() 68 | local cur_status = {} 69 | 70 | if not nixio.fs.stat(sync_status_file) then 71 | io.open(sync_status_file, "w"):close() 72 | end 73 | 74 | get_lock() 75 | 76 | for line in io.lines(sync_status_file) 77 | do 78 | local key, value = line:match("^(.*)\t(.*)$") 79 | cur_status[key] = value 80 | end 81 | 82 | local f = io.open(sync_status_file, "w") 83 | if f then 84 | for key, value in pairs(sync_status) 85 | do 86 | f:write(key .. "\t" .. value .. "\n") 87 | end 88 | for key, value in pairs(cur_status) 89 | do 90 | if not sync_status[key] then 91 | f:write(key .. "\t" .. value .. "\n") 92 | end 93 | end 94 | f:close() 95 | end 96 | 97 | release_lock() 98 | end 99 | 100 | function merge_messages() 101 | local rmsg = {} 102 | local lmsg = {} 103 | 104 | for line in io.lines(meshchat_path .. "/remote_messages") 105 | do 106 | local key = line:match("^(%S+)%s") 107 | rmsg[key] = line 108 | end 109 | 110 | get_lock() 111 | 112 | for line in io.lines(messages_db_file) 113 | do 114 | local key = line:match("^(%S+)%s") 115 | lmsg[key] = line 116 | end 117 | 118 | local f = io.open(messages_db_file, "a") 119 | if f then 120 | for rmsg_id, line in pairs(rmsg) 121 | do 122 | if not lmsg[rmsg_id] then 123 | f:write(line .. "\n") 124 | end 125 | end 126 | f:close() 127 | end 128 | 129 | sort_and_trim_db() 130 | 131 | save_messages_db_version() 132 | 133 | release_lock() 134 | end 135 | 136 | function merge_users() 137 | local rusers = {} 138 | local lusers = {} 139 | 140 | for line in io.lines(meshchat_path .. "/remote_users") 141 | do 142 | local key, value = line:match("^(%S+\t%S+\t%S+)\t(.*)$") 143 | if not line:match("error") and key then 144 | rusers[key] = value 145 | end 146 | end 147 | 148 | get_lock() 149 | 150 | for line in io.lines(remote_users_status_file) 151 | do 152 | local key, value = line:match("^(%S+\t%S+\t%S+)\t(.*)$") 153 | if not line:match("error") and key then 154 | lusers[key] = value 155 | end 156 | end 157 | 158 | local f = io.open(remote_users_status_file, "w") 159 | if f then 160 | for key, _ in pairs(rusers) 161 | do 162 | if lusers[key] and lusers[key] > rusers[key] then 163 | f:write(key .. "\t" .. lusers[key] .. "\n") 164 | else 165 | f:write(key .. "\t" .. rusers[key] .. "\n") 166 | end 167 | end 168 | for key, _ in pairs(lusers) 169 | do 170 | if not rusers[key] then 171 | f:write(key .. "\t" .. lusers[key] .. "\n") 172 | end 173 | end 174 | f:close() 175 | end 176 | 177 | release_lock() 178 | end 179 | 180 | while true 181 | do 182 | local nodes = node_list() 183 | 184 | sync_status = {} 185 | 186 | for _, node_info in ipairs(nodes) 187 | do 188 | for _ = 1,1 189 | do 190 | local remote_node = node_info.node 191 | local remote_platform = node_info.platform 192 | local remote_port = node_info.port 193 | 194 | local port = "" 195 | if remote_port ~= "" then 196 | port = ":" .. remote_port 197 | end 198 | 199 | if port == "" and remote_platform == "node" then 200 | port = ":8080" 201 | end 202 | 203 | local version = get_messages_db_version() 204 | 205 | -- Poll non mesh chat nodes at a longer interval 206 | if non_mesh_chat_nodes[remote_node] and os.time() < non_mesh_chat_nodes[remote_node] then 207 | break 208 | end 209 | 210 | nixio.fs.remove(meshchat_path .. "/remote_users") 211 | 212 | -- Get remote users file 213 | local f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " -sD - \"http://" .. remote_node .. port .. "/cgi-bin/meshchat?action=users_raw&platform=" .. platform .. "&node=" .. node .. "\" -o " .. meshchat_path .. "/remote_users 2>&1") 214 | local output = f:read("*a") 215 | f:close() 216 | 217 | -- Check if meshchat is installed 218 | if output:match("404 Not Found") then 219 | non_mesh_chat_nodes[remote_node] = os.time() + non_meshchat_poll_interval 220 | break 221 | end 222 | 223 | local md5 = output:match("Content%-MD5:%s([0-9a-f]+)\r\n") 224 | if md5 then 225 | local f_md5 = file_md5(meshchat_path .. "/remote_users") 226 | if md5 == f_md5 then 227 | local cur_size = nixio.fs.stat(meshchat_path .. "/remote_users").size 228 | if cur_size > 0 then 229 | merge_users() 230 | end 231 | end 232 | end 233 | 234 | -- Get remote files file 235 | nixio.fs.remove(meshchat_path .. "/remote_files") 236 | f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " -sD - \"http://" .. remote_node .. port .. "/cgi-bin/meshchat?action=local_files_raw\" -o " .. meshchat_path .. "/remote_files 2>&1") 237 | output = f:read("*a") 238 | f:close() 239 | 240 | md5 = output:match("Content%-MD5:%s([0-9a-f]+)\r\n") 241 | if md5 then 242 | local f_md5 = file_md5(meshchat_path .. "/remote_files") 243 | nixio.fs.remove(meshchat_path .. "/remote_files." .. remote_node) 244 | if md5 == f_md5 then 245 | local cur_size = nixio.fs.stat(meshchat_path .. "/remote_files").size 246 | if cur_size > 0 then 247 | nixio.fs.rename(meshchat_path .. "/remote_files", meshchat_path .. "/remote_files." .. remote_node) 248 | end 249 | end 250 | end 251 | 252 | -- Get remote messages 253 | nixio.fs.remove(meshchat_path .. "/remote_messages") 254 | 255 | f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " \"http://" .. remote_node .. port .. "/cgi-bin/meshchat?action=messages_version\" -o - 2> /dev/null") 256 | local remote_version = f:read("*a") 257 | f:close() 258 | 259 | -- Check the version of the remote db against ours. Only download the db if the remote has a different copy 260 | 261 | if remote_version ~= "" and version == remote_version then 262 | sync_status[remote_node] = os.time() 263 | break 264 | end 265 | 266 | f = io.popen("/usr/bin/curl --retry 0 --connect-timeout " .. connect_timeout .. " --speed-time " .. speed_time .. " --speed-limit " .. speed_limit .. " -sD - \"http://" .. remote_node .. port .. "/cgi-bin/meshchat?action=messages_raw\" -o " .. meshchat_path .. "/remote_messages 2>&1") 267 | local output = f:read("*a") 268 | f:close() 269 | 270 | if nixio.fs.stat(meshchat_path .. "/remote_messages") then 271 | local md5 = output:match("Content%-MD5:%s([0-9a-f]+)\r\n") 272 | if md5 then 273 | local f_md5 = file_md5(meshchat_path .. "/remote_messages") 274 | if md5 == f_md5 then 275 | local cur_size = nixio.fs.stat(meshchat_path .. "/remote_messages").size 276 | if cur_size > 0 then 277 | sync_status[remote_node] = os.time() 278 | merge_messages() 279 | end 280 | end 281 | end 282 | end 283 | end 284 | end 285 | 286 | log_status() 287 | 288 | nixio.fs.remove(meshchat_path .. "/remote_messages") 289 | nixio.fs.remove(meshchat_path .. "/remote_users") 290 | nixio.fs.remove(meshchat_path .. "/remote_files") 291 | 292 | nixio.nanosleep(poll_interval, 0) 293 | end 294 | -------------------------------------------------------------------------------- /support/meshchatsync-init.d: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=99 4 | APP=meshchatsync 5 | SERVICE_WRITE_PID=1 6 | SERVICE_DAEMONIZE=1 7 | 8 | start() { 9 | service_start /usr/local/bin/meshchatsync 10 | } 11 | stop() { 12 | service_stop /usr/local/bin/meshchatsync 13 | killall meshchatsync 14 | } 15 | -------------------------------------------------------------------------------- /www/alert.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hickey/meshchat/bd4c2436a80c7d78c2097b2582ff5ac65ac6fa3d/www/alert.mp3 -------------------------------------------------------------------------------- /www/chat.js: -------------------------------------------------------------------------------- 1 | var meshchat_id; 2 | var last_messages_update = epoch(); 3 | var call_sign = 'NOCALL'; 4 | var enable_video = 0; 5 | 6 | var messages = new Messages(); 7 | let alert = new Audio('alert.mp3'); 8 | 9 | let context = { 10 | config_loaded: false, 11 | debug: true, // let startup funcs show debug 12 | } 13 | 14 | $(function() { 15 | meshchat_init(); 16 | }); 17 | 18 | function monitor_last_update() { 19 | var secs = epoch() - last_messages_update; 20 | $('#last-update').html('Updated: ' + secs + ' seconds ago'); 21 | } 22 | 23 | function update_messages(reason=Messages.MSG_UPDATE) { 24 | if (reason != Messages.MSG_UPDATE) return; 25 | try { 26 | var caller = (new Error()).stack.split("\n")[3].split("/")[0]; 27 | } 28 | catch (TypeError) { 29 | var caller = "unknown_caller"; 30 | } 31 | console.debug(caller + "->update_messages(reason=MSG_UPDATE)"); 32 | 33 | // update the message table 34 | let html = messages.render($('#channels').val(), $('#search').val()); 35 | if (html) $('#message-table').html(html); 36 | last_messages_update = epoch(); 37 | } 38 | 39 | function new_messages(reason) { 40 | if (reason != Messages.NEW_MSG) return; 41 | try { 42 | var caller = (new Error()).stack.split("\n")[3].split("/")[0]; 43 | } 44 | catch (TypeError) { 45 | var caller = "unknown_caller"; 46 | } 47 | console.debug(caller + "->new_messages(reason=NEW_MSG)"); 48 | alert.play(); 49 | } 50 | 51 | function update_channels(reason) { 52 | if (reason != Messages.CHAN_UPDATE) return; 53 | try { 54 | var caller = (new Error()).stack.split("\n")[3].split("/")[0]; 55 | } 56 | catch (TypeError) { 57 | var caller = "unknown_caller"; 58 | } 59 | console.debug(caller + "->update_channels(reason=CHAN_UPDATE)"); 60 | 61 | let msg_refresh = false; 62 | let channels = messages.channels().sort(); 63 | let channel_filter = $('#channels').val(); 64 | let cur_send_channel = $('#send-channel').val(); 65 | // null signals a new channel was just created 66 | if (cur_send_channel == null) { 67 | channel_filter = messages.current_channel(); 68 | cur_send_channel = messages.current_channel(); 69 | msg_refresh = true; 70 | } 71 | 72 | // clear channel selection boxes 73 | $('#send-channel').find('option').remove().end(); 74 | $('#channels').find('option').remove().end(); 75 | 76 | function add_option(select, title, value) { 77 | select.append(""); 78 | } 79 | 80 | // Add static channels to channel selection boxes 81 | add_option($('#send-channel'), "Everything", ""); 82 | add_option($('#send-channel'), "Add New Channel", "Add New Channel"); 83 | add_option($('#channels'), "Everything", ""); 84 | 85 | for (var chan of channels) { 86 | if (chan != "") { 87 | add_option($('#send-channel'), chan, chan); 88 | add_option($('#channels'), chan, chan); 89 | } 90 | } 91 | 92 | $("#channels").val(channel_filter); 93 | $("#send-channel").val(cur_send_channel); 94 | if (msg_refresh) update_messages(); 95 | } 96 | 97 | function start_chat() { 98 | debug("start_chat()"); 99 | 100 | // wait until the configuration is fully loaded 101 | $.getJSON('/cgi-bin/meshchat?action=config', 102 | (data) => { 103 | config = data; 104 | document.title = 'Mesh Chat v' + data.version; 105 | $('#version').html('Mesh Chat v' + data.version + ''); 106 | $('#node').html('Node: ' + data.node); 107 | $('#zone').html('Zone: ' + data.zone); 108 | $('#callsign').html('Call Sign: ' + Cookies.get('meshchat_call_sign')); 109 | $('#copyright').html('Mesh Chat v' + data.version + ' Copyright © ' + new Date().getFullYear() + ' Trevor Paskett - K7FPV (Lua by KN6PLV)'); 110 | 111 | if ("default_channel" in data) { 112 | default_channel = data.default_channel; 113 | $('#send-channel').val(data.default_channel); 114 | $('#channels').val(data.default_channel); 115 | messages.set_channel(data.default_channel); 116 | update_messages(); 117 | } 118 | 119 | if ("debug" in data) { 120 | context.debug = data.debug == 1 ? true : false; 121 | } 122 | 123 | // signal that the config has finished loading 124 | context.config_loaded = true; 125 | } 126 | ).fail( 127 | (error) => { 128 | // TODO error message on UI describing failure 129 | error("Failed to load configuration from config API: " + error); 130 | } 131 | ); 132 | 133 | //$('#logout').html('Logout ' + call_sign); 134 | messages.subscribe(update_messages); 135 | messages.subscribe(new_messages); 136 | messages.subscribe(update_channels); 137 | messages.check(); 138 | load_users(); 139 | monitor_last_update(); 140 | 141 | // start event loops to update MeshChat client 142 | setInterval(() => { messages.check() }, 15000); 143 | setInterval(() => { load_users() }, 15000); 144 | setInterval(() => { monitor_last_update() }, 2500); 145 | } 146 | 147 | function meshchat_init() { 148 | debug("meshchat_init()"); 149 | 150 | $('#message').val(''); 151 | meshchat_id = Cookies.get('meshchat_id'); 152 | if (meshchat_id == undefined) { 153 | // TODO set default expiration of cookie 154 | Cookies.set('meshchat_id', make_id()); 155 | meshchat_id = Cookies.get('meshchat_id'); 156 | } 157 | 158 | $('#submit-message').on('click', function(e) { 159 | e.preventDefault(); 160 | if ($('#message').val().length == 0) return; 161 | 162 | ohSnapX(); 163 | 164 | // disable message sending box 165 | $(this).prop("disabled", true); 166 | $('#message').prop("disabled", true); 167 | $(this).html('
'); 168 | 169 | let channel = $('#send-channel').val(); 170 | 171 | if ($('#new-channel').val() != '') { 172 | channel = $('#new-channel').val(); 173 | $('#send-channel').val('Everything'); 174 | } 175 | 176 | messages.send($('#message').val(), channel, call_sign).then( 177 | // sent 178 | (sent) => { 179 | $('#message').val(''); 180 | ohSnap('Message sent', 'green'); 181 | update_messages(Messages.NEW_MSG); 182 | 183 | // clear out new channel box in case it was used and 184 | // reset to normal selection box 185 | $('#new-channel').val(''); 186 | $('#new-channel').hide(); 187 | $('#send-channel').show(); 188 | }, 189 | // error 190 | (err_msg) => { 191 | ohSnap(err_msg, 'red', {time: '30000'}); 192 | } 193 | ).finally(() => { 194 | // change the channel selector to the channel the message was 195 | // just sent to 196 | $('#channels').val(channel); 197 | messages.set_channel(channel); 198 | update_messages(); 199 | 200 | // re-enable message sending box 201 | $(this).prop("disabled", false); 202 | $('#message').prop("disabled", false); 203 | $(this).html('Send'); 204 | }); 205 | }); 206 | 207 | $('#submit-call-sign').on('click', function(e) { 208 | e.preventDefault(); 209 | if ($('#call-sign').val().length == 0) return; 210 | call_sign = $('#call-sign').val().toUpperCase(); 211 | // TODO set default expiration of cookie 212 | Cookies.set('meshchat_call_sign', call_sign); 213 | $('#call-sign-container').addClass('hidden'); 214 | $('#chat-container').removeClass('hidden'); 215 | $('#callsign').html('Call Sign: ' + call_sign); 216 | start_chat(); 217 | }); 218 | 219 | $('#channels').on('change', function() { 220 | $('#send-channel').val(this.value); 221 | messages.set_channel(this.value); 222 | update_messages(); 223 | }); 224 | 225 | $('#search').keyup(function() { 226 | //console.log(this.value); 227 | update_messages(); 228 | }); 229 | 230 | $('#message-expand').on('click', function(e) { 231 | $('#message-panel').toggleClass('message-panel-collapse'); 232 | $('#message-panel-body').toggleClass('message-panel-body-collapse'); 233 | $('#users-panel').toggleClass('users-panel-collapse'); 234 | $('#users-panel-body').toggleClass('users-panel-body-collapse'); 235 | }); 236 | 237 | // allow user to enter new channel 238 | $('#send-channel').on('change', function() { 239 | if (this.value == "Add New Channel") { 240 | $('#new-channel').show().focus(); 241 | $(this).hide(); 242 | } 243 | }); 244 | 245 | // process a CTRL to send a message 246 | $('#message').keydown(function (e) { 247 | if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) { 248 | $("#submit-message").trigger( "click" ); 249 | } 250 | }); 251 | 252 | // login with a cookie 253 | var cookie_call_sign = Cookies.get('meshchat_call_sign'); 254 | if (cookie_call_sign == undefined) { 255 | $('#call-sign-container').removeClass('hidden'); 256 | } else { 257 | $('#call-sign-container').addClass('hidden'); 258 | $('#chat-container').removeClass('hidden'); 259 | call_sign = cookie_call_sign; 260 | start_chat(); 261 | } 262 | } 263 | 264 | let users_updating = false; 265 | function load_users() { 266 | debug("load_users()"); 267 | 268 | if (users_updating == true) return; 269 | console.debug("load_users()"); 270 | 271 | // lock to prevent simultaneous updates 272 | users_updating = true; 273 | 274 | $.getJSON('/cgi-bin/meshchat?action=users&call_sign=' + call_sign + '&id=' + meshchat_id, 275 | (data) => { 276 | if (data == null || data == 0) return; 277 | 278 | let html = ''; 279 | let count = 0; 280 | 281 | for (var entry of data) { 282 | var date = new Date(0); 283 | date.setUTCSeconds(entry.epoch); 284 | 285 | // user heartbeat timeout > 4 mins 286 | if ((epoch() - entry.epoch) > 240) continue; 287 | 288 | // user heartbeat > 2 mins, expiring 289 | if ((epoch() - entry.epoch) > 120) { 290 | html += ''; 291 | } else { 292 | html += ''; 293 | } 294 | 295 | if (enable_video == 0) { 296 | html += '' + entry.call_sign + ''; 297 | } else { 298 | html += '' + entry.call_sign + ''; 299 | } 300 | 301 | if (entry.platform == 'node') { 302 | html += '' + entry.node + ''; 303 | } else { 304 | html += '' + entry.node + ''; 305 | } 306 | 307 | html += '' + format_date(date) + ''; 308 | html += ''; 309 | 310 | count++; 311 | } 312 | $('#users-table').html(html); 313 | $('#users-count').html(count); 314 | }).always(() => { 315 | // allow updates again 316 | users_updating = false; 317 | }); 318 | } 319 | -------------------------------------------------------------------------------- /www/files.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mesh Chat 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 27 |
28 |
29 | 30 | Mesh Chat 31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | Updated: 46 | 47 | 0 secs ago 48 |
49 |
50 |
51 |
52 |
53 |
54 | Upload a File 55 |
56 |
57 |
58 | 59 |
60 | 61 | 62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | File System 70 |
71 |
72 |
73 |
74 | 75 |
76 | 79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | Files Available0 Files 91 |
92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |
FileSizeNodeTime
113 |
114 |
115 |
116 |
117 |
118 |
119 | 120 |
121 |
122 |
123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /www/files.js: -------------------------------------------------------------------------------- 1 | var last_update = epoch(); 2 | var free_space = 0; 3 | 4 | function monitor_last_update() { 5 | var secs = epoch() - last_update; 6 | $('#last-update').html('Updated: ' + secs + ' seconds ago'); 7 | } 8 | 9 | $(function() { 10 | load_files(); 11 | setInterval(function() { 12 | load_files() 13 | }, 30000); 14 | setInterval(function() { monitor_last_update() }, 2500); 15 | var file = null; 16 | $('#upload-file').on("change", function(event) { 17 | file = event.target.files[0]; 18 | console.log(event.target.files[0].size); 19 | if (event.target.files[0].size > free_space) { 20 | ohSnap('Not enough free space for your file, delete some files first and try again', 'red'); 21 | $('#upload-file').val(''); 22 | event.preventDefault(); 23 | } 24 | }); 25 | $('#download-messages').on('click', function(e) { 26 | e.preventDefault(); 27 | location.href = '/cgi-bin/meshchat?action=messages_download'; 28 | }); 29 | $("#upload-button").on("click", function(event) { 30 | event.preventDefault(); 31 | //$('#upload-form').submit(); 32 | var file_data = new FormData(); 33 | if (file == null) return; 34 | file_data.append('uploadfile', file); 35 | $.ajax({ 36 | url: '/cgi-bin/meshchat?action=upload_file', 37 | type: "POST", 38 | data: file_data, 39 | dataType: "json", 40 | context: this, 41 | cache: false, 42 | processData: false, 43 | contentType: false, 44 | beforeSend: function() { 45 | $('progress').removeClass('hidden'); 46 | }, 47 | xhr: function() { 48 | var myXhr = $.ajaxSettings.xhr(); 49 | if (myXhr.upload) { 50 | myXhr.upload.addEventListener('progress', upload_progress, false); 51 | } 52 | return myXhr; 53 | }, 54 | success: function(data) { 55 | if (data.status == 200) { 56 | ohSnap('File uploaded', 'green'); 57 | } else { 58 | ohSnap(data.response, 'red'); 59 | } 60 | $('#upload-file').val(''); 61 | load_files(); 62 | }, 63 | error: function(data, textStatus, errorThrown) { 64 | ohSnap('File upload error'); 65 | }, 66 | complete: function(jqXHR, textStatus) { 67 | $('progress').addClass('hidden'); 68 | } 69 | }); 70 | }); 71 | }); 72 | 73 | function upload_progress(event) { 74 | if (event.lengthComputable) { 75 | $('progress').attr({ 76 | value: event.loaded, 77 | max: event.total 78 | }); 79 | } 80 | } 81 | 82 | function fileNameCompare(a, b) { 83 | if (a.file < b.file) 84 | return -1; 85 | if (a.file > b.file) 86 | return 1; 87 | return 0; 88 | } 89 | 90 | function load_files() { 91 | $.getJSON('/cgi-bin/meshchat?action=files', function(data) { 92 | var html = ''; 93 | 94 | data.files.sort(fileNameCompare); 95 | 96 | for (var i = 0; i < data.files.length; i++) { 97 | var date = new Date(0); 98 | date.setUTCSeconds(data.files[i].epoch); 99 | html += ''; 100 | var port = ''; 101 | 102 | //console.log(data); 103 | 104 | if (data.files[i].node.match(':')) { 105 | var parts = data.files[i].node.split(':'); 106 | data.files[i].node = parts[0]; 107 | port = ':' + parts[1]; 108 | } else { 109 | if (data.files[i].platform == 'node') { 110 | port = ':8080' 111 | } 112 | } 113 | html += '' + data.files[i].file + ''; 114 | html += '' + numeral(data.files[i].size).format('0.0 b') + ''; 115 | html += '' + data.files[i].node + ''; 116 | html += '' + format_date(date) + ''; 117 | if (data.files[i].local == 1) { 118 | html += ''; 119 | } else { 120 | html += ''; 121 | } 122 | html += ''; 123 | } 124 | $('#files-table').html(html); 125 | $('#files-count').html(data.files.length + ' Files'); 126 | $('#total-bytes').html('Total Storage: ' + numeral(data.stats.allowed).format('0.0 b')); 127 | $('#free-bytes').html('Free Storage: ' + numeral(data.stats.files_free).format('0.0 b')); 128 | free_space = data.stats.files_free; 129 | $(".delete-button").on("click", function(event) { 130 | event.preventDefault(); 131 | $.ajax({ 132 | url: '/cgi-bin/meshchat?action=delete_file&file=' + encodeURIComponent($(this).attr('file-name')), 133 | type: "GET", 134 | success: function(data) { 135 | ohSnap('File deleted', 'green'); 136 | load_files(); 137 | }, 138 | error: function(data, textStatus, errorThrown) { 139 | ohSnap('File delete error: ' + data, 'red'); 140 | } 141 | }); 142 | }); 143 | 144 | last_update = epoch(); 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mesh Chat 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 47 | 233 | 235 | 237 | 239 | 241 | 243 | 245 | 247 | 248 | 249 | -------------------------------------------------------------------------------- /www/js.cookie.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * JavaScript Cookie v2.0.4 3 | * https://github.com/js-cookie/js-cookie 4 | * 5 | * Copyright 2006, 2015 Klaus Hartl & Fagner Brack 6 | * Released under the MIT license 7 | */ 8 | (function (factory) { 9 | if (typeof define === 'function' && define.amd) { 10 | define(factory); 11 | } else if (typeof exports === 'object') { 12 | module.exports = factory(); 13 | } else { 14 | var _OldCookies = window.Cookies; 15 | var api = window.Cookies = factory(); 16 | api.noConflict = function () { 17 | window.Cookies = _OldCookies; 18 | return api; 19 | }; 20 | } 21 | }(function () { 22 | function extend () { 23 | var i = 0; 24 | var result = {}; 25 | for (; i < arguments.length; i++) { 26 | var attributes = arguments[ i ]; 27 | for (var key in attributes) { 28 | result[key] = attributes[key]; 29 | } 30 | } 31 | return result; 32 | } 33 | 34 | function init (converter) { 35 | function api (key, value, attributes) { 36 | var result; 37 | 38 | // Write 39 | 40 | if (arguments.length > 1) { 41 | attributes = extend({ 42 | path: '/' 43 | }, api.defaults, attributes); 44 | 45 | if (typeof attributes.expires === 'number') { 46 | var expires = new Date(); 47 | expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); 48 | attributes.expires = expires; 49 | } 50 | 51 | try { 52 | result = JSON.stringify(value); 53 | if (/^[\{\[]/.test(result)) { 54 | value = result; 55 | } 56 | } catch (e) {} 57 | 58 | if (!converter.write) { 59 | value = encodeURIComponent(String(value)) 60 | .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); 61 | } else { 62 | value = converter.write(value, key); 63 | } 64 | 65 | key = encodeURIComponent(String(key)); 66 | key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); 67 | key = key.replace(/[\(\)]/g, escape); 68 | 69 | return (document.cookie = [ 70 | key, '=', value, 71 | attributes.expires && '; expires=' + attributes.expires.toUTCString(), // use expires attribute, max-age is not supported by IE 72 | attributes.path && '; path=' + attributes.path, 73 | attributes.domain && '; domain=' + attributes.domain, 74 | attributes.secure ? '; secure' : '' 75 | ].join('')); 76 | } 77 | 78 | // Read 79 | 80 | if (!key) { 81 | result = {}; 82 | } 83 | 84 | // To prevent the for loop in the first place assign an empty array 85 | // in case there are no cookies at all. Also prevents odd result when 86 | // calling "get()" 87 | var cookies = document.cookie ? document.cookie.split('; ') : []; 88 | var rdecode = /(%[0-9A-Z]{2})+/g; 89 | var i = 0; 90 | 91 | for (; i < cookies.length; i++) { 92 | var parts = cookies[i].split('='); 93 | var name = parts[0].replace(rdecode, decodeURIComponent); 94 | var cookie = parts.slice(1).join('='); 95 | 96 | if (cookie.charAt(0) === '"') { 97 | cookie = cookie.slice(1, -1); 98 | } 99 | 100 | try { 101 | cookie = converter.read ? 102 | converter.read(cookie, name) : converter(cookie, name) || 103 | cookie.replace(rdecode, decodeURIComponent); 104 | 105 | if (this.json) { 106 | try { 107 | cookie = JSON.parse(cookie); 108 | } catch (e) {} 109 | } 110 | 111 | if (key === name) { 112 | result = cookie; 113 | break; 114 | } 115 | 116 | if (!key) { 117 | result[name] = cookie; 118 | } 119 | } catch (e) {} 120 | } 121 | 122 | return result; 123 | } 124 | 125 | api.get = api.set = api; 126 | api.getJSON = function () { 127 | return api.apply({ 128 | json: true 129 | }, [].slice.call(arguments)); 130 | }; 131 | api.defaults = {}; 132 | 133 | api.remove = function (key, attributes) { 134 | api(key, '', extend(attributes, { 135 | expires: -1 136 | })); 137 | }; 138 | 139 | api.withConverter = init; 140 | 141 | return api; 142 | } 143 | 144 | return init(function () {}); 145 | })); 146 | -------------------------------------------------------------------------------- /www/md5.js: -------------------------------------------------------------------------------- 1 | /* 2 | MD5 code copyright (c) by Joseph Myers 3 | http://www.myersdaily.org/joseph/javascript/md5-text.html 4 | */ 5 | function md5cycle(x, k) { 6 | var a = x[0], b = x[1], c = x[2], d = x[3]; 7 | 8 | a = ff(a, b, c, d, k[0], 7, -680876936); 9 | d = ff(d, a, b, c, k[1], 12, -389564586); 10 | c = ff(c, d, a, b, k[2], 17, 606105819); 11 | b = ff(b, c, d, a, k[3], 22, -1044525330); 12 | a = ff(a, b, c, d, k[4], 7, -176418897); 13 | d = ff(d, a, b, c, k[5], 12, 1200080426); 14 | c = ff(c, d, a, b, k[6], 17, -1473231341); 15 | b = ff(b, c, d, a, k[7], 22, -45705983); 16 | a = ff(a, b, c, d, k[8], 7, 1770035416); 17 | d = ff(d, a, b, c, k[9], 12, -1958414417); 18 | c = ff(c, d, a, b, k[10], 17, -42063); 19 | b = ff(b, c, d, a, k[11], 22, -1990404162); 20 | a = ff(a, b, c, d, k[12], 7, 1804603682); 21 | d = ff(d, a, b, c, k[13], 12, -40341101); 22 | c = ff(c, d, a, b, k[14], 17, -1502002290); 23 | b = ff(b, c, d, a, k[15], 22, 1236535329); 24 | 25 | a = gg(a, b, c, d, k[1], 5, -165796510); 26 | d = gg(d, a, b, c, k[6], 9, -1069501632); 27 | c = gg(c, d, a, b, k[11], 14, 643717713); 28 | b = gg(b, c, d, a, k[0], 20, -373897302); 29 | a = gg(a, b, c, d, k[5], 5, -701558691); 30 | d = gg(d, a, b, c, k[10], 9, 38016083); 31 | c = gg(c, d, a, b, k[15], 14, -660478335); 32 | b = gg(b, c, d, a, k[4], 20, -405537848); 33 | a = gg(a, b, c, d, k[9], 5, 568446438); 34 | d = gg(d, a, b, c, k[14], 9, -1019803690); 35 | c = gg(c, d, a, b, k[3], 14, -187363961); 36 | b = gg(b, c, d, a, k[8], 20, 1163531501); 37 | a = gg(a, b, c, d, k[13], 5, -1444681467); 38 | d = gg(d, a, b, c, k[2], 9, -51403784); 39 | c = gg(c, d, a, b, k[7], 14, 1735328473); 40 | b = gg(b, c, d, a, k[12], 20, -1926607734); 41 | 42 | a = hh(a, b, c, d, k[5], 4, -378558); 43 | d = hh(d, a, b, c, k[8], 11, -2022574463); 44 | c = hh(c, d, a, b, k[11], 16, 1839030562); 45 | b = hh(b, c, d, a, k[14], 23, -35309556); 46 | a = hh(a, b, c, d, k[1], 4, -1530992060); 47 | d = hh(d, a, b, c, k[4], 11, 1272893353); 48 | c = hh(c, d, a, b, k[7], 16, -155497632); 49 | b = hh(b, c, d, a, k[10], 23, -1094730640); 50 | a = hh(a, b, c, d, k[13], 4, 681279174); 51 | d = hh(d, a, b, c, k[0], 11, -358537222); 52 | c = hh(c, d, a, b, k[3], 16, -722521979); 53 | b = hh(b, c, d, a, k[6], 23, 76029189); 54 | a = hh(a, b, c, d, k[9], 4, -640364487); 55 | d = hh(d, a, b, c, k[12], 11, -421815835); 56 | c = hh(c, d, a, b, k[15], 16, 530742520); 57 | b = hh(b, c, d, a, k[2], 23, -995338651); 58 | 59 | a = ii(a, b, c, d, k[0], 6, -198630844); 60 | d = ii(d, a, b, c, k[7], 10, 1126891415); 61 | c = ii(c, d, a, b, k[14], 15, -1416354905); 62 | b = ii(b, c, d, a, k[5], 21, -57434055); 63 | a = ii(a, b, c, d, k[12], 6, 1700485571); 64 | d = ii(d, a, b, c, k[3], 10, -1894986606); 65 | c = ii(c, d, a, b, k[10], 15, -1051523); 66 | b = ii(b, c, d, a, k[1], 21, -2054922799); 67 | a = ii(a, b, c, d, k[8], 6, 1873313359); 68 | d = ii(d, a, b, c, k[15], 10, -30611744); 69 | c = ii(c, d, a, b, k[6], 15, -1560198380); 70 | b = ii(b, c, d, a, k[13], 21, 1309151649); 71 | a = ii(a, b, c, d, k[4], 6, -145523070); 72 | d = ii(d, a, b, c, k[11], 10, -1120210379); 73 | c = ii(c, d, a, b, k[2], 15, 718787259); 74 | b = ii(b, c, d, a, k[9], 21, -343485551); 75 | 76 | x[0] = add32(a, x[0]); 77 | x[1] = add32(b, x[1]); 78 | x[2] = add32(c, x[2]); 79 | x[3] = add32(d, x[3]); 80 | } 81 | 82 | function cmn(q, a, b, x, s, t) { 83 | a = add32(add32(a, q), add32(x, t)); 84 | return add32((a << s) | (a >>> (32 - s)), b); 85 | } 86 | 87 | function ff(a, b, c, d, x, s, t) { 88 | return cmn((b & c) | ((~b) & d), a, b, x, s, t); 89 | } 90 | 91 | function gg(a, b, c, d, x, s, t) { 92 | return cmn((b & d) | (c & (~d)), a, b, x, s, t); 93 | } 94 | 95 | function hh(a, b, c, d, x, s, t) { 96 | return cmn(b ^ c ^ d, a, b, x, s, t); 97 | } 98 | 99 | function ii(a, b, c, d, x, s, t) { 100 | return cmn(c ^ (b | (~d)), a, b, x, s, t); 101 | } 102 | 103 | function md51(s) { 104 | txt = ''; 105 | var n = s.length, 106 | state = [1732584193, -271733879, -1732584194, 271733878], i; 107 | 108 | for (i=64; i<=s.length; i+=64) { 109 | md5cycle(state, md5blk(s.substring(i-64, i))); 110 | } 111 | 112 | s = s.substring(i-64); 113 | var tail = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]; 114 | for (i=0; i>2] |= s.charCodeAt(i) << ((i%4) << 3); 116 | tail[i>>2] |= 0x80 << ((i%4) << 3); 117 | 118 | if (i > 55) { 119 | md5cycle(state, tail); 120 | for (i=0; i<16; i++) tail[i] = 0; 121 | } 122 | 123 | tail[14] = n*8; 124 | md5cycle(state, tail); 125 | return state; 126 | } 127 | 128 | /* there needs to be support for Unicode here, 129 | * unless we pretend that we can redefine the MD-5 130 | * algorithm for multi-byte characters (perhaps 131 | * by adding every four 16-bit characters and 132 | * shortening the sum to 32 bits). Otherwise 133 | * I suggest performing MD-5 as if every character 134 | * was two bytes--e.g., 0040 0025 = @%--but then 135 | * how will an ordinary MD-5 sum be matched? 136 | * There is no way to standardize text to something 137 | * like UTF-8 before transformation; speed cost is 138 | * utterly prohibitive. The JavaScript standard 139 | * itself needs to look at this: it should start 140 | * providing access to strings as preformed UTF-8 141 | * 8-bit unsigned value arrays. 142 | */ 143 | function md5blk(s) { /* I figured global was faster. */ 144 | var md5blks = [], i; /* Andy King said do it this way. */ 145 | 146 | for (i=0; i<64; i+=4) { 147 | md5blks[i>>2] = s.charCodeAt(i) 148 | + (s.charCodeAt(i+1) << 8) 149 | + (s.charCodeAt(i+2) << 16) 150 | + (s.charCodeAt(i+3) << 24); 151 | } 152 | return md5blks; 153 | } 154 | 155 | var hex_chr = '0123456789abcdef'.split(''); 156 | 157 | function rhex(n) { 158 | var s='', j=0; 159 | 160 | for(; j<4; j++) 161 | s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] 162 | + hex_chr[(n >> (j * 8)) & 0x0F]; 163 | return s; 164 | } 165 | 166 | function hex(x) { 167 | for (var i=0; i> 16) + (y >> 16) + (lsw >> 16); 190 | return (msw << 16) | (lsw & 0xFFFF); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /www/messages.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Messages is a singleton that keeps a copy of all messages 4 | class Messages { 5 | 6 | static NEW_MSG = 1; 7 | static CHAN_UPDATE = 2; 8 | static MSG_UPDATE = 3; 9 | 10 | constructor() { 11 | if (! this.__instance) { 12 | this.messages = new Map(); 13 | this.message_order = new Array(); 14 | this.delete_list = new Array(); // future enhancement 15 | this.db_version = 0; 16 | this.last_update_time = 0; 17 | this._updating = false; 18 | this._message_checksum = null; // only messages in channel 19 | 20 | this.__current_channel = ""; 21 | this.__channels = new Array(); 22 | this.__observers = new Array(); 23 | 24 | this.__instance = this; 25 | } 26 | return this.__instance; 27 | } 28 | 29 | // return reference to singleton, creating if necessary 30 | getInstance() { 31 | if (! this.__instance) { 32 | this.__instance = new Messages(); 33 | } 34 | return this.__instance; 35 | } 36 | 37 | /* check() retrieves the current message database version from the 38 | MeshChat server and compares it with the last known version. 39 | If the database version is different (i.e. database has new messages), 40 | then an update cycle is kicked off by calling fetch() */ 41 | check() { 42 | console.debug("Messages.check()"); 43 | 44 | var pending_db_version = 0; 45 | 46 | // currently updating, ignore this check 47 | if (this._updating == true) { 48 | console.debug("Message.check() skipped due to messages being updated."); 49 | return; 50 | } 51 | 52 | // lock out all other updates 53 | this._updating = true; 54 | 55 | $.getJSON('/cgi-bin/meshchat?action=messages_version_ui&call_sign=' + call_sign + '&id=' + meshchat_id + '&epoch=' + epoch(), 56 | (data) => { 57 | if (data == null || data == 0) { 58 | this._updating = false; 59 | } else if ('messages_version' in data && this.db_version != data.messages_version) { 60 | this.fetch(data.messages_version); 61 | } else { 62 | this._updating = false; 63 | } 64 | }).fail((error) => { 65 | // TODO error message on UI describing failure 66 | this._updating = false; 67 | }); 68 | } 69 | 70 | /* fetch() is used to retrieve the messages from the message database. 71 | It is told the new database version with the pending_version param. 72 | All messages are then stored in the local message db (this.messages) 73 | and update() is called to update all the internal counters */ 74 | fetch(pending_version) { 75 | console.debug("Messages.fetch(pending_version = " + pending_version + ")"); 76 | 77 | $.getJSON('/cgi-bin/meshchat?action=messages&call_sign=' + call_sign + '&id=' + meshchat_id + '&epoch=' + epoch(), 78 | (data) => { 79 | if (data == null || data == 0) empty(); 80 | 81 | // integrate new messages into the message DB 82 | data.forEach((entry) => { this.messages.set(entry.id, entry) }); 83 | 84 | this.update(); 85 | this.last_update_time = epoch(); 86 | this.db_version = pending_version; 87 | this._updating = false; 88 | this.notify(Messages.MSG_UPDATE); 89 | this.notify(Messages.CHAN_UPDATE); 90 | }).fail((error) => { 91 | // TODO error message on UI describing failure 92 | this._updating = false; 93 | }); 94 | } 95 | 96 | /* update the message DB with counts, channels, etc. 97 | If msg_ids is not specified, then process all messages in the 98 | DB */ 99 | update(msg_ids=null) { 100 | console.debug("Messages.update(msg_ids=" + JSON.stringify(msg_ids) + " )"); 101 | 102 | if (msg_ids === null) { 103 | msg_ids = Array.from(this.messages.keys()); 104 | } 105 | 106 | for (var id of msg_ids.values()) { 107 | var message = this.messages.get(id); 108 | 109 | // if there is not a message don't try to process it. 110 | if (message === undefined) { 111 | // throw error message 112 | continue; 113 | } 114 | 115 | // null channel names is the Everything channel (empty string) 116 | if (message.channel === null) { 117 | message.channel = ""; 118 | } 119 | 120 | // update list of available channels 121 | if (! this.__channels.includes(message.channel)) { 122 | this.__channels.push(message.channel); 123 | } 124 | 125 | // TODO not sure this is actually needed, get should be returning a reference 126 | this.messages.set(id, message); 127 | } 128 | 129 | // sort the messages by time (descending) 130 | this.message_order = Array.from(this.messages.keys()).sort( 131 | (a,b) => { 132 | let a_msg = this.messages.get(a); 133 | let b_msg = this.messages.get(b); 134 | return a_msg.epoch > b_msg.epoch ? -1 : 1; 135 | }); 136 | } 137 | 138 | set_channel(chan) { 139 | console.debug("Messages.set_channel(chan=" + chan + ")"); 140 | this.__current_channel = chan; 141 | this._message_checksum = null; 142 | } 143 | 144 | current_channel() { 145 | return this.__current_channel; 146 | } 147 | 148 | // return a list of channels available across all messages 149 | channels() { 150 | return Array.from(this.__channels.values()); 151 | } 152 | 153 | send(message, channel, call_sign) { 154 | console.debug("Messages.send(message='" + message +"', channel=" + channel + ", call_sign=" + call_sign + ")"); 155 | let params = { 156 | action: 'send_message', 157 | message: message, 158 | call_sign: call_sign, 159 | epoch: epoch(), 160 | id: this._create_id(), 161 | channel: channel 162 | }; 163 | 164 | // { timeout: 5000, retryLimit: 3, dataType: "json", data: params} 165 | return new Promise((sent, error) => { 166 | $.post('/cgi-bin/meshchat', params, 167 | (data) => { 168 | if (data.status == 500) { 169 | error('Error sending message: ' + data.response); 170 | } else { 171 | // add the message to the in memory message DB 172 | this.messages.set(params['id'], { 173 | id: params['id'], 174 | message: message, 175 | call_sign: call_sign, 176 | epoch: params['epoch'], 177 | channel: channel, 178 | node: node_name(), 179 | platform: platform(), 180 | }); 181 | 182 | // Add the channel to the list 183 | if (! channel in this.channels()) { 184 | this.__channels.push(channel); 185 | this.set_channel(channel); 186 | this.notify(Messages.CHAN_UPDATE); 187 | } 188 | 189 | // update internal message checksum with locally 190 | // created message ID so not to trigger alert sound 191 | this._message_checksum += parseInt(params['id'], 16); 192 | this.update(); 193 | this.notify(Messages.MSG_UPDATE); 194 | sent(); 195 | } 196 | }).fail((error) => { 197 | if (error == 'timeout') { 198 | this.tryCount++; 199 | if (this.tryCount <= this.retryLimit) { 200 | //try again 201 | $.ajax(this); 202 | return; 203 | } 204 | error(error); 205 | } 206 | }); 207 | }) 208 | 209 | } 210 | 211 | // return a rendered version of a block of messages 212 | render(channel, search_filter) { 213 | console.debug("Messages.render(channel=" + channel + ", search_filter=" + search_filter + ")"); 214 | let html = ''; 215 | let search = search_filter.toLowerCase(); 216 | let message_checksum = 0; 217 | 218 | for (var id of this.message_order) { 219 | var message = this.messages.get(id); 220 | 221 | // calculate the date for the current message 222 | let date = new Date(0); 223 | date.setUTCSeconds(message.epoch); 224 | message.date = date; 225 | 226 | if (search != '') { 227 | //console.log(search); 228 | //console.log(message.toLowerCase()); 229 | if (message.message.toLowerCase().search(search) == -1 && 230 | message.call_sign.toLowerCase().search(search) == -1 && 231 | message.node.toLowerCase().search(search) == -1 && 232 | format_date(date).toLowerCase().search(search) == -1) { 233 | continue; 234 | } 235 | } 236 | 237 | if (channel == message.channel || this.__current_channel == '') { 238 | html += this.render_row(message); 239 | 240 | // add this message to the checksum 241 | message_checksum += parseInt(message.id, 16); 242 | } 243 | } 244 | 245 | // provide a message if no messages were found 246 | if (html == "") { 247 | html = "No messages found"; 248 | } 249 | 250 | // this._message_checksum == null is the first rendering of the 251 | // message table. No need to sound an alert. 252 | if (this._message_checksum != null && message_checksum != this._message_checksum) { 253 | this.notify(Messages.NEW_MSG); 254 | } 255 | this._message_checksum = message_checksum; 256 | 257 | return html; 258 | } 259 | 260 | render_row(msg_data) { 261 | let message = msg_data.message.replace(/(\r\n|\n|\r)/g, "
"); 262 | 263 | let row = ''; 264 | if (false) { 265 | row += '' + msg_data.id + ''; 266 | } 267 | row += '' + format_date(msg_data.date) + ''; 268 | row += '' + message + ''; 269 | row += '' + msg_data.call_sign + ''; 270 | row += '' + msg_data.channel + ''; 271 | if (msg_data.platform == 'node') { 272 | row += '' + msg_data.node + ''; 273 | } else { 274 | row += '' + msg_data.node + ''; 275 | } 276 | row += ''; 277 | 278 | return row; 279 | } 280 | 281 | // generate unique message IDs 282 | _create_id() { 283 | let seed = epoch().toString() + Math.floor(Math.random() * 99999); 284 | let hash = md5(seed); 285 | return hash.substring(0,8); 286 | } 287 | 288 | // Observer functions 289 | subscribe(func) { 290 | console.debug("Messages.subscribe(func=" + func.name + ")"); 291 | this.__observers.push(func); 292 | } 293 | 294 | unsubscribe(func) { 295 | console.debug("Messages.unsubscribe(func=" + func + ")"); 296 | this.__observers = this.__observers.filter((observer) => observer !== func); 297 | } 298 | 299 | notify(reason) { 300 | console.debug("Messages.notify(reason=" + reason + ")"); 301 | this.__observers.forEach((observer) => observer(reason)); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /www/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } -------------------------------------------------------------------------------- /www/numeral.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * numeral.js 3 | * version : 1.5.3 4 | * author : Adam Draper 5 | * license : MIT 6 | * http://adamwdraper.github.com/Numeral-js/ 7 | */ 8 | (function(){function a(a){this._value=a}function b(a,b,c,d){var e,f,g=Math.pow(10,b);return f=(c(a*g)/g).toFixed(b),d&&(e=new RegExp("0{1,"+d+"}$"),f=f.replace(e,"")),f}function c(a,b,c){var d;return d=b.indexOf("$")>-1?e(a,b,c):b.indexOf("%")>-1?f(a,b,c):b.indexOf(":")>-1?g(a,b):i(a._value,b,c)}function d(a,b){var c,d,e,f,g,i=b,j=["KB","MB","GB","TB","PB","EB","ZB","YB"],k=!1;if(b.indexOf(":")>-1)a._value=h(b);else if(b===q)a._value=0;else{for("."!==o[p].delimiters.decimal&&(b=b.replace(/\./g,"").replace(o[p].delimiters.decimal,".")),c=new RegExp("[^a-zA-Z]"+o[p].abbreviations.thousand+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),d=new RegExp("[^a-zA-Z]"+o[p].abbreviations.million+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),e=new RegExp("[^a-zA-Z]"+o[p].abbreviations.billion+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),f=new RegExp("[^a-zA-Z]"+o[p].abbreviations.trillion+"(?:\\)|(\\"+o[p].currency.symbol+")?(?:\\))?)?$"),g=0;g<=j.length&&!(k=b.indexOf(j[g])>-1?Math.pow(1024,g+1):!1);g++);a._value=(k?k:1)*(i.match(c)?Math.pow(10,3):1)*(i.match(d)?Math.pow(10,6):1)*(i.match(e)?Math.pow(10,9):1)*(i.match(f)?Math.pow(10,12):1)*(b.indexOf("%")>-1?.01:1)*((b.split("-").length+Math.min(b.split("(").length-1,b.split(")").length-1))%2?1:-1)*Number(b.replace(/[^0-9\.]+/g,"")),a._value=k?Math.ceil(a._value):a._value}return a._value}function e(a,b,c){var d,e,f=b.indexOf("$"),g=b.indexOf("("),h=b.indexOf("-"),j="";return b.indexOf(" $")>-1?(j=" ",b=b.replace(" $","")):b.indexOf("$ ")>-1?(j=" ",b=b.replace("$ ","")):b=b.replace("$",""),e=i(a._value,b,c),1>=f?e.indexOf("(")>-1||e.indexOf("-")>-1?(e=e.split(""),d=1,(g>f||h>f)&&(d=0),e.splice(d,0,o[p].currency.symbol+j),e=e.join("")):e=o[p].currency.symbol+j+e:e.indexOf(")")>-1?(e=e.split(""),e.splice(-1,0,j+o[p].currency.symbol),e=e.join("")):e=e+j+o[p].currency.symbol,e}function f(a,b,c){var d,e="",f=100*a._value;return b.indexOf(" %")>-1?(e=" ",b=b.replace(" %","")):b=b.replace("%",""),d=i(f,b,c),d.indexOf(")")>-1?(d=d.split(""),d.splice(-1,0,e+"%"),d=d.join("")):d=d+e+"%",d}function g(a){var b=Math.floor(a._value/60/60),c=Math.floor((a._value-60*b*60)/60),d=Math.round(a._value-60*b*60-60*c);return b+":"+(10>c?"0"+c:c)+":"+(10>d?"0"+d:d)}function h(a){var b=a.split(":"),c=0;return 3===b.length?(c+=60*Number(b[0])*60,c+=60*Number(b[1]),c+=Number(b[2])):2===b.length&&(c+=60*Number(b[0]),c+=Number(b[1])),Number(c)}function i(a,c,d){var e,f,g,h,i,j,k=!1,l=!1,m=!1,n="",r=!1,s=!1,t=!1,u=!1,v=!1,w="",x="",y=Math.abs(a),z=["B","KB","MB","GB","TB","PB","EB","ZB","YB"],A="",B=!1;if(0===a&&null!==q)return q;if(c.indexOf("(")>-1?(k=!0,c=c.slice(1,-1)):c.indexOf("+")>-1&&(l=!0,c=c.replace(/\+/g,"")),c.indexOf("a")>-1&&(r=c.indexOf("aK")>=0,s=c.indexOf("aM")>=0,t=c.indexOf("aB")>=0,u=c.indexOf("aT")>=0,v=r||s||t||u,c.indexOf(" a")>-1?(n=" ",c=c.replace(" a","")):c=c.replace("a",""),y>=Math.pow(10,12)&&!v||u?(n+=o[p].abbreviations.trillion,a/=Math.pow(10,12)):y=Math.pow(10,9)&&!v||t?(n+=o[p].abbreviations.billion,a/=Math.pow(10,9)):y=Math.pow(10,6)&&!v||s?(n+=o[p].abbreviations.million,a/=Math.pow(10,6)):(y=Math.pow(10,3)&&!v||r)&&(n+=o[p].abbreviations.thousand,a/=Math.pow(10,3))),c.indexOf("b")>-1)for(c.indexOf(" b")>-1?(w=" ",c=c.replace(" b","")):c=c.replace("b",""),g=0;g<=z.length;g++)if(e=Math.pow(1024,g),f=Math.pow(1024,g+1),a>=e&&f>a){w+=z[g],e>0&&(a/=e);break}return c.indexOf("o")>-1&&(c.indexOf(" o")>-1?(x=" ",c=c.replace(" o","")):c=c.replace("o",""),x+=o[p].ordinal(a)),c.indexOf("[.]")>-1&&(m=!0,c=c.replace("[.]",".")),h=a.toString().split(".")[0],i=c.split(".")[1],j=c.indexOf(","),i?(i.indexOf("[")>-1?(i=i.replace("]",""),i=i.split("["),A=b(a,i[0].length+i[1].length,d,i[1].length)):A=b(a,i.length,d),h=A.split(".")[0],A=A.split(".")[1].length?o[p].delimiters.decimal+A.split(".")[1]:"",m&&0===Number(A.slice(1))&&(A="")):h=b(a,null,d),h.indexOf("-")>-1&&(h=h.slice(1),B=!0),j>-1&&(h=h.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g,"$1"+o[p].delimiters.thousands)),0===c.indexOf(".")&&(h=""),(k&&B?"(":"")+(!k&&B?"-":"")+(!B&&l?"+":"")+h+A+(x?x:"")+(n?n:"")+(w?w:"")+(k&&B?")":"")}function j(a,b){o[a]=b}function k(a){var b=a.toString().split(".");return b.length<2?1:Math.pow(10,b[1].length)}function l(){var a=Array.prototype.slice.call(arguments);return a.reduce(function(a,b){var c=k(a),d=k(b);return c>d?c:d},-1/0)}var m,n="1.5.3",o={},p="en",q=null,r="0,0",s="undefined"!=typeof module&&module.exports;m=function(b){return m.isNumeral(b)?b=b.value():0===b||"undefined"==typeof b?b=0:Number(b)||(b=m.fn.unformat(b)),new a(Number(b))},m.version=n,m.isNumeral=function(b){return b instanceof a},m.language=function(a,b){if(!a)return p;if(a&&!b){if(!o[a])throw new Error("Unknown language : "+a);p=a}return(b||!o[a])&&j(a,b),m},m.languageData=function(a){if(!a)return o[p];if(!o[a])throw new Error("Unknown language : "+a);return o[a]},m.language("en",{delimiters:{thousands:",",decimal:"."},abbreviations:{thousand:"k",million:"m",billion:"b",trillion:"t"},ordinal:function(a){var b=a%10;return 1===~~(a%100/10)?"th":1===b?"st":2===b?"nd":3===b?"rd":"th"},currency:{symbol:"$"}}),m.zeroFormat=function(a){q="string"==typeof a?a:null},m.defaultFormat=function(a){r="string"==typeof a?a:"0.0"},"function"!=typeof Array.prototype.reduce&&(Array.prototype.reduce=function(a,b){"use strict";if(null===this||"undefined"==typeof this)throw new TypeError("Array.prototype.reduce called on null or undefined");if("function"!=typeof a)throw new TypeError(a+" is not a function");var c,d,e=this.length>>>0,f=!1;for(1c;++c)this.hasOwnProperty(c)&&(f?d=a(d,this[c],c,this):(d=this[c],f=!0));if(!f)throw new TypeError("Reduce of empty array with no initial value");return d}),m.fn=a.prototype={clone:function(){return m(this)},format:function(a,b){return c(this,a?a:r,void 0!==b?b:Math.round)},unformat:function(a){return"[object Number]"===Object.prototype.toString.call(a)?a:d(this,a?a:r)},value:function(){return this._value},valueOf:function(){return this._value},set:function(a){return this._value=Number(a),this},add:function(a){function b(a,b){return a+c*b}var c=l.call(null,this._value,a);return this._value=[this._value,a].reduce(b,0)/c,this},subtract:function(a){function b(a,b){return a-c*b}var c=l.call(null,this._value,a);return this._value=[a].reduce(b,this._value*c)/c,this},multiply:function(a){function b(a,b){var c=l(a,b);return a*c*b*c/(c*c)}return this._value=[this._value,a].reduce(b,1),this},divide:function(a){function b(a,b){var c=l(a,b);return a*c/(b*c)}return this._value=[this._value,a].reduce(b),this},difference:function(a){return Math.abs(m(this._value).subtract(a).value())}},s&&(module.exports=m),"undefined"==typeof ender&&(this.numeral=m),"function"==typeof define&&define.amd&&define([],function(){return m})}).call(this); -------------------------------------------------------------------------------- /www/ohsnap.js: -------------------------------------------------------------------------------- 1 | /** 2 | * == OhSnap!.js == 3 | * A simple jQuery/Zepto notification library designed to be used in mobile apps 4 | * 5 | * author: Justin Domingue 6 | * date: september 18, 2015 7 | * version: 0.1.4 8 | * copyright - nice copyright over here 9 | */ 10 | 11 | /* Shows a toast on the page 12 | * Params: 13 | * text: text to show 14 | * color: color of the toast. one of red, green, blue, orange, yellow or custom 15 | */ 16 | function ohSnap(text, color, icon) { 17 | var icon_markup = "", 18 | html, 19 | time = '5000', 20 | $container = $('#ohsnap'); 21 | 22 | if (icon) { 23 | icon_markup = " "; 24 | } 25 | 26 | // Generate the HTML 27 | html = $('
' + icon_markup + text + '
').fadeIn('fast'); 28 | 29 | // Append the label to the container 30 | $container.append(html); 31 | 32 | // Remove the notification on click 33 | html.on('click', function() { 34 | ohSnapX($(this)); 35 | }); 36 | 37 | // After 'time' seconds, the animation fades out 38 | setTimeout(function() { 39 | ohSnapX(html); 40 | }, time); 41 | } 42 | 43 | function ohSnapX(element) { 44 | // Called without argument, the function removes all alerts 45 | // element must be a jQuery object 46 | 47 | if (typeof element !== "undefined") { 48 | element.fadeOut('fast', function() { 49 | $(this).remove(); 50 | }); 51 | } else { 52 | $('.alert').fadeOut('fast', function() { 53 | $(this).remove(); 54 | }); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /www/shared.js: -------------------------------------------------------------------------------- 1 | var config; 2 | 3 | $(function() { 4 | $('#logout').on('click', function(e){ 5 | e.preventDefault(); 6 | Cookies.remove('meshchat_call_sign'); 7 | window.location = '/meshchat'; 8 | }); 9 | }); 10 | 11 | function node_name() { 12 | return config.node; 13 | } 14 | 15 | function platform() { 16 | return config.platform || 'node'; // TODO temp patch until config API is updated 17 | } 18 | 19 | function epoch() { 20 | return Math.floor(new Date() / 1000); 21 | } 22 | 23 | function format_date(date) { 24 | var string; 25 | 26 | var year = String(date.getFullYear()); 27 | 28 | string = (date.getMonth()+1) + '/' + date.getDate() + '/' + year.slice(-2); 29 | string += '
'; 30 | 31 | var hours = date.getHours(); 32 | var minutes = date.getMinutes(); 33 | var ampm = hours >= 12 ? 'PM' : 'AM'; 34 | 35 | hours = hours % 12; 36 | hours = hours ? hours : 12; // the hour '0' should be '12' 37 | minutes = minutes < 10 ? '0'+minutes : minutes; 38 | 39 | string += hours + ':' + minutes + ' ' + ampm; 40 | 41 | return string; 42 | } 43 | 44 | function make_id() 45 | { 46 | var text = ""; 47 | var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 48 | 49 | for( var i=0; i < 5; i++ ) 50 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 51 | 52 | return text; 53 | } 54 | 55 | function aredn_domain(host) { 56 | if (host.indexOf(".") !== -1) { 57 | return host; 58 | } 59 | host = host.split(":") 60 | return host[0] + ".local.mesh" + (host[1] ? ":" + host[1] : ""); 61 | } 62 | 63 | function debug(msg) { 64 | context.debug && console.debug(msg); 65 | } 66 | 67 | function error(msg) { 68 | console.error(msg); 69 | } 70 | -------------------------------------------------------------------------------- /www/skeleton.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Skeleton V2.0.4 3 | * Copyright 2014, Dave Gamache 4 | * www.getskeleton.com 5 | * Free to use under the MIT license. 6 | * http://www.opensource.org/licenses/mit-license.php 7 | * 12/29/2014 8 | */ 9 | 10 | 11 | /* Table of contents 12 | –––––––––––––––––––––––––––––––––––––––––––––––––– 13 | - Grid 14 | - Base Styles 15 | - Typography 16 | - Links 17 | - Buttons 18 | - Forms 19 | - Lists 20 | - Code 21 | - Tables 22 | - Spacing 23 | - Utilities 24 | - Clearing 25 | - Media Queries 26 | */ 27 | 28 | 29 | /* Grid 30 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 31 | .container { 32 | position: relative; 33 | width: 100%; 34 | max-width: 960px; 35 | margin: 0 auto; 36 | padding: 0 20px; 37 | box-sizing: border-box; } 38 | .column, 39 | .columns { 40 | width: 100%; 41 | float: left; 42 | box-sizing: border-box; } 43 | 44 | /* For devices larger than 400px */ 45 | @media (min-width: 400px) { 46 | .container { 47 | width: 85%; 48 | padding: 0; } 49 | } 50 | 51 | /* For devices larger than 550px */ 52 | @media (min-width: 550px) { 53 | .container { 54 | width: 80%; } 55 | .column, 56 | .columns { 57 | margin-left: 4%; } 58 | .column:first-child, 59 | .columns:first-child { 60 | margin-left: 0; } 61 | 62 | .one.column, 63 | .one.columns { width: 4.66666666667%; } 64 | .two.columns { width: 13.3333333333%; } 65 | .three.columns { width: 22%; } 66 | .four.columns { width: 30.6666666667%; } 67 | .five.columns { width: 39.3333333333%; } 68 | .six.columns { width: 48%; } 69 | .seven.columns { width: 56.6666666667%; } 70 | .eight.columns { width: 65.3333333333%; } 71 | .nine.columns { width: 74.0%; } 72 | .ten.columns { width: 82.6666666667%; } 73 | .eleven.columns { width: 91.3333333333%; } 74 | .twelve.columns { width: 100%; margin-left: 0; } 75 | 76 | .one-third.column { width: 30.6666666667%; } 77 | .two-thirds.column { width: 65.3333333333%; } 78 | 79 | .one-half.column { width: 48%; } 80 | 81 | /* Offsets */ 82 | .offset-by-one.column, 83 | .offset-by-one.columns { margin-left: 8.66666666667%; } 84 | .offset-by-two.column, 85 | .offset-by-two.columns { margin-left: 17.3333333333%; } 86 | .offset-by-three.column, 87 | .offset-by-three.columns { margin-left: 26%; } 88 | .offset-by-four.column, 89 | .offset-by-four.columns { margin-left: 34.6666666667%; } 90 | .offset-by-five.column, 91 | .offset-by-five.columns { margin-left: 43.3333333333%; } 92 | .offset-by-six.column, 93 | .offset-by-six.columns { margin-left: 52%; } 94 | .offset-by-seven.column, 95 | .offset-by-seven.columns { margin-left: 60.6666666667%; } 96 | .offset-by-eight.column, 97 | .offset-by-eight.columns { margin-left: 69.3333333333%; } 98 | .offset-by-nine.column, 99 | .offset-by-nine.columns { margin-left: 78.0%; } 100 | .offset-by-ten.column, 101 | .offset-by-ten.columns { margin-left: 86.6666666667%; } 102 | .offset-by-eleven.column, 103 | .offset-by-eleven.columns { margin-left: 95.3333333333%; } 104 | 105 | .offset-by-one-third.column, 106 | .offset-by-one-third.columns { margin-left: 34.6666666667%; } 107 | .offset-by-two-thirds.column, 108 | .offset-by-two-thirds.columns { margin-left: 69.3333333333%; } 109 | 110 | .offset-by-one-half.column, 111 | .offset-by-one-half.columns { margin-left: 52%; } 112 | 113 | } 114 | 115 | 116 | /* Base Styles 117 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 118 | /* NOTE 119 | html is set to 62.5% so that all the REM measurements throughout Skeleton 120 | are based on 10px sizing. So basically 1.5rem = 15px :) */ 121 | html { 122 | font-size: 62.5%; } 123 | body { 124 | font-size: 1.5em; /* currently ems cause chrome bug misinterpreting rems on body element */ 125 | line-height: 1.6; 126 | font-weight: 400; 127 | font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif; 128 | color: #222; } 129 | 130 | 131 | /* Typography 132 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 133 | h1, h2, h3, h4, h5, h6 { 134 | margin-top: 0; 135 | margin-bottom: 2rem; 136 | font-weight: 300; } 137 | h1 { font-size: 4.0rem; line-height: 1.2; letter-spacing: -.1rem;} 138 | h2 { font-size: 3.6rem; line-height: 1.25; letter-spacing: -.1rem; } 139 | h3 { font-size: 3.0rem; line-height: 1.3; letter-spacing: -.1rem; } 140 | h4 { font-size: 2.4rem; line-height: 1.35; letter-spacing: -.08rem; } 141 | h5 { font-size: 1.8rem; line-height: 1.5; letter-spacing: -.05rem; } 142 | h6 { font-size: 1.5rem; line-height: 1.6; letter-spacing: 0; } 143 | 144 | /* Larger than phablet */ 145 | @media (min-width: 550px) { 146 | h1 { font-size: 5.0rem; } 147 | h2 { font-size: 4.2rem; } 148 | h3 { font-size: 3.6rem; } 149 | h4 { font-size: 3.0rem; } 150 | h5 { font-size: 2.4rem; } 151 | h6 { font-size: 1.5rem; } 152 | } 153 | 154 | p { 155 | margin-top: 0; } 156 | 157 | 158 | /* Links 159 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 160 | a { 161 | color: #1EAEDB; } 162 | a:hover { 163 | color: #0FA0CE; } 164 | 165 | 166 | /* Buttons 167 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 168 | .button, 169 | button, 170 | input[type="submit"], 171 | input[type="reset"], 172 | input[type="button"] { 173 | display: inline-block; 174 | height: 38px; 175 | padding: 0 30px; 176 | color: #555; 177 | text-align: center; 178 | font-size: 11px; 179 | font-weight: 600; 180 | line-height: 38px; 181 | letter-spacing: .1rem; 182 | text-transform: uppercase; 183 | text-decoration: none; 184 | white-space: nowrap; 185 | background-color: transparent; 186 | border-radius: 4px; 187 | border: 1px solid #bbb; 188 | cursor: pointer; 189 | box-sizing: border-box; } 190 | .button:hover, 191 | button:hover, 192 | input[type="submit"]:hover, 193 | input[type="reset"]:hover, 194 | input[type="button"]:hover, 195 | .button:focus, 196 | button:focus, 197 | input[type="submit"]:focus, 198 | input[type="reset"]:focus, 199 | input[type="button"]:focus { 200 | color: #333; 201 | border-color: #888; 202 | outline: 0; } 203 | .button.button-primary, 204 | button.button-primary, 205 | input[type="submit"].button-primary, 206 | input[type="reset"].button-primary, 207 | input[type="button"].button-primary { 208 | color: #FFF; 209 | background-color: #33C3F0; 210 | border-color: #33C3F0; } 211 | .button.button-primary:hover, 212 | button.button-primary:hover, 213 | input[type="submit"].button-primary:hover, 214 | input[type="reset"].button-primary:hover, 215 | input[type="button"].button-primary:hover, 216 | .button.button-primary:focus, 217 | button.button-primary:focus, 218 | input[type="submit"].button-primary:focus, 219 | input[type="reset"].button-primary:focus, 220 | input[type="button"].button-primary:focus { 221 | color: #FFF; 222 | background-color: #1EAEDB; 223 | border-color: #1EAEDB; } 224 | 225 | 226 | /* Forms 227 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 228 | input[type="email"], 229 | input[type="number"], 230 | input[type="search"], 231 | input[type="text"], 232 | input[type="tel"], 233 | input[type="url"], 234 | input[type="password"], 235 | textarea, 236 | select { 237 | height: 38px; 238 | padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */ 239 | background-color: #fff; 240 | border: 1px solid #D1D1D1; 241 | border-radius: 4px; 242 | box-shadow: none; 243 | box-sizing: border-box; } 244 | /* Removes awkward default styles on some inputs for iOS */ 245 | input[type="email"], 246 | input[type="number"], 247 | input[type="search"], 248 | input[type="text"], 249 | input[type="tel"], 250 | input[type="url"], 251 | input[type="password"], 252 | textarea { 253 | -webkit-appearance: none; 254 | -moz-appearance: none; 255 | appearance: none; } 256 | textarea { 257 | min-height: 65px; 258 | padding-top: 6px; 259 | padding-bottom: 6px; } 260 | input[type="email"]:focus, 261 | input[type="number"]:focus, 262 | input[type="search"]:focus, 263 | input[type="text"]:focus, 264 | input[type="tel"]:focus, 265 | input[type="url"]:focus, 266 | input[type="password"]:focus, 267 | textarea:focus, 268 | select:focus { 269 | border: 1px solid #33C3F0; 270 | outline: 0; } 271 | label, 272 | legend { 273 | display: block; 274 | margin-bottom: .5rem; 275 | font-weight: 600; } 276 | fieldset { 277 | padding: 0; 278 | border-width: 0; } 279 | input[type="checkbox"], 280 | input[type="radio"] { 281 | display: inline; } 282 | label > .label-body { 283 | display: inline-block; 284 | margin-left: .5rem; 285 | font-weight: normal; } 286 | 287 | 288 | /* Lists 289 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 290 | ul { 291 | list-style: circle inside; } 292 | ol { 293 | list-style: decimal inside; } 294 | ol, ul { 295 | padding-left: 0; 296 | margin-top: 0; } 297 | ul ul, 298 | ul ol, 299 | ol ol, 300 | ol ul { 301 | margin: 1.5rem 0 1.5rem 3rem; 302 | font-size: 90%; } 303 | li { 304 | margin-bottom: 1rem; } 305 | 306 | 307 | /* Code 308 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 309 | code { 310 | padding: .2rem .5rem; 311 | margin: 0 .2rem; 312 | font-size: 90%; 313 | white-space: nowrap; 314 | background: #F1F1F1; 315 | border: 1px solid #E1E1E1; 316 | border-radius: 4px; } 317 | pre > code { 318 | display: block; 319 | padding: 1rem 1.5rem; 320 | white-space: pre; } 321 | 322 | 323 | /* Tables 324 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 325 | th, 326 | td { 327 | padding: 12px 15px; 328 | text-align: left; 329 | border-bottom: 1px solid #E1E1E1; } 330 | th:first-child, 331 | td:first-child { 332 | padding-left: 0; } 333 | th:last-child, 334 | td:last-child { 335 | padding-right: 0; } 336 | 337 | 338 | /* Spacing 339 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 340 | button, 341 | .button { 342 | margin-bottom: 1rem; } 343 | input, 344 | textarea, 345 | select, 346 | fieldset { 347 | margin-bottom: 1.5rem; } 348 | pre, 349 | blockquote, 350 | dl, 351 | figure, 352 | table, 353 | p, 354 | ul, 355 | ol, 356 | form { 357 | margin-bottom: 2.5rem; } 358 | 359 | 360 | /* Utilities 361 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 362 | .u-full-width { 363 | width: 100%; 364 | box-sizing: border-box; } 365 | .u-max-full-width { 366 | max-width: 100%; 367 | box-sizing: border-box; } 368 | .u-pull-right { 369 | float: right; } 370 | .u-pull-left { 371 | float: left; } 372 | 373 | 374 | /* Misc 375 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 376 | hr { 377 | margin-top: 3rem; 378 | margin-bottom: 3.5rem; 379 | border-width: 0; 380 | border-top: 1px solid #E1E1E1; } 381 | 382 | 383 | /* Clearing 384 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 385 | 386 | /* Self Clearing Goodness */ 387 | .container:after, 388 | .row:after, 389 | .u-cf { 390 | content: ""; 391 | display: table; 392 | clear: both; } 393 | 394 | 395 | /* Media Queries 396 | –––––––––––––––––––––––––––––––––––––––––––––––––– */ 397 | /* 398 | Note: The best way to structure the use of media queries is to create the queries 399 | near the relevant code. For example, if you wanted to change the styles for buttons 400 | on small devices, paste the mobile query code up in the buttons section and style it 401 | there. 402 | */ 403 | 404 | 405 | /* Larger than mobile */ 406 | @media (min-width: 400px) {} 407 | 408 | /* Larger than phablet (also point when grid becomes active) */ 409 | @media (min-width: 550px) {} 410 | 411 | /* Larger than tablet */ 412 | @media (min-width: 750px) {} 413 | 414 | /* Larger than desktop */ 415 | @media (min-width: 1000px) {} 416 | 417 | /* Larger than Desktop HD */ 418 | @media (min-width: 1200px) {} 419 | -------------------------------------------------------------------------------- /www/status.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Mesh Chat 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 27 |
28 |
29 | 30 | Mesh Chat 31 | 32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | Updated: 46 | 47 | 0 secs ago 48 |
49 |
50 |
51 |
52 |
53 |
54 | Node Sync Status0 55 |
56 |
57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
NodeLast Sync
67 |
68 |
69 |
70 |
71 | 94 |
95 |
96 | 97 |
98 |
99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /www/status.js: -------------------------------------------------------------------------------- 1 | var last_update = epoch(); 2 | 3 | $(function() { 4 | load_status(); 5 | setInterval(function(){ load_status() }, 5000); 6 | setInterval(function() { monitor_last_update() }, 2500); 7 | }); 8 | 9 | function monitor_last_update() { 10 | var secs = epoch() - last_update; 11 | $('#last-update').html('Updated: ' + secs + ' seconds ago'); 12 | } 13 | 14 | function load_status() { 15 | $.getJSON('/cgi-bin/meshchat?action=sync_status', function(data) { 16 | var html = ''; 17 | var count = 0; 18 | 19 | for (var i = 0; i < data.length; i++) { 20 | var date = new Date(0); 21 | date.setUTCSeconds(data[i].epoch); 22 | 23 | //if ((epoch() - data[i].epoch) > 60 * 60) continue; 24 | 25 | html += ''; 26 | html += '' + data[i].node + ''; 27 | html += '' + format_date(date) + ''; 28 | html += ''; 29 | 30 | count++; 31 | } 32 | 33 | $('#sync-table').html(html); 34 | $('#sync-count').html(count); 35 | 36 | last_update = epoch(); 37 | }); 38 | 39 | $.getJSON('/cgi-bin/meshchat?action=action_log', function(data) { 40 | var html = ''; 41 | 42 | for (var i = 0; i < data.length; i++) { 43 | var date = new Date(0); 44 | date.setUTCSeconds(data[i].action_epoch); 45 | 46 | html += ''; 47 | html += '' + format_date(date) + ''; 48 | html += '' + data[i].script + ''; 49 | html += '' + data[i].result + ''; 50 | html += '' + data[i].message + ''; 51 | html += ''; 52 | } 53 | 54 | $('#log-table').html(html); 55 | 56 | last_update = epoch(); 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /www/style.css: -------------------------------------------------------------------------------- 1 | body{ margin: 0; } 2 | input { 3 | -webkit-appearance: none; 4 | border-radius: 0; 5 | } 6 | 7 | #call-sign { 8 | text-transform: uppercase; 9 | } 10 | 11 | #copyright { 12 | margin-top: 10px; 13 | } 14 | 15 | .pull-right { 16 | float: right; 17 | } 18 | 19 | .alert { 20 | padding: 15px; 21 | margin-bottom: 20px; 22 | border: 1px solid #eed3d7; 23 | border-radius: 4px; 24 | } 25 | 26 | .alert-red { 27 | color: white; 28 | background-color: #DA4453; 29 | } 30 | 31 | .alert-green { 32 | color: white; 33 | background-color: #37BC9B; 34 | } 35 | 36 | .alert-blue { 37 | color: white; 38 | background-color: #4A89DC; 39 | } 40 | 41 | .alert-yellow { 42 | color: white; 43 | background-color: #F6BB42; 44 | } 45 | 46 | .alert-orange { 47 | color: white; 48 | background-color: #E9573F; 49 | } 50 | 51 | td:first-child { 52 | white-space: nowrap; 53 | } 54 | 55 | .hidden { 56 | display: none; 57 | } 58 | 59 | .navbar { 60 | border-top-width: 0; 61 | border-bottom: 1px solid #eee; 62 | } 63 | 64 | .navbar, .navbar-spacer { 65 | display: block; 66 | width: 100%; 67 | height: 6.5rem; 68 | background: #fff; 69 | z-index: 99; 70 | margin-bottom: 0px; 71 | } 72 | 73 | .navbar-spacer { 74 | display: none; 75 | } 76 | 77 | .navbar > .container { 78 | width: 100%; 79 | padding: 0px; 80 | } 81 | 82 | .navbar-list { 83 | list-style: none; 84 | margin-bottom: 0; 85 | } 86 | 87 | .navbar-item { 88 | position: relative; 89 | float: left; 90 | margin-bottom: 0; 91 | } 92 | 93 | .navbar-link { 94 | text-transform: uppercase; 95 | font-size: 12px; 96 | font-weight: 600; 97 | margin-right: 15px; 98 | text-decoration: none; 99 | line-height: 6.5rem; 100 | color: #222; 101 | } 102 | 103 | .navbar-link.active { 104 | color: #33C3F0; 105 | } 106 | 107 | .has-docked-nav .navbar { 108 | position: fixed; 109 | top: 0; 110 | left: 0; 111 | } 112 | 113 | .has-docked-nav .navbar-spacer { 114 | display: block; 115 | } 116 | 117 | 118 | /* Re-overiding the width 100% declaration to match size of % based container */ 119 | 120 | .has-docked-nav .navbar > .container { 121 | width: 80%; 122 | } 123 | 124 | .grey-background { 125 | background-color: grey; 126 | } 127 | 128 | progress { 129 | color: #337ab7; 130 | width: 100%; 131 | } 132 | 133 | #message { 134 | min-height: 65px; 135 | font-size: 1em; 136 | } 137 | 138 | #message-parent-table { 139 | table-layout: fixed; 140 | width: 100%; 141 | } 142 | 143 | video { 144 | width: 100% !important; 145 | height: auto !important; 146 | } 147 | 148 | body { 149 | font-size: 1.2em; 150 | } 151 | 152 | th, td { 153 | padding-top: 5px; 154 | padding-bottom: 5px; 155 | padding-right: 0px; 156 | } 157 | 158 | table { 159 | border-collapse: collapse; 160 | } 161 | 162 | table td { 163 | word-wrap: break-word; 164 | overflow-wrap: break-word; 165 | } 166 | 167 | textarea:focus { 168 | border: 1px solid #337ab7; 169 | } 170 | 171 | textarea { 172 | resize: none; 173 | } 174 | 175 | button.button-primary:hover { 176 | background-color: #337ab7; 177 | border-color: #337ab7; 178 | } 179 | 180 | .button, button, input[type="submit"], input[type="reset"], input[type="button"]{ line-height: 1; } 181 | 182 | .center { 183 | text-align: center; 184 | } 185 | 186 | .right { 187 | text-align: right; 188 | } 189 | 190 | .info { 191 | margin-top: 5px; 192 | } 193 | 194 | .updated { 195 | margin-bottom: 5px; 196 | } 197 | 198 | #send-channel-label { 199 | display: inline; 200 | } 201 | 202 | #send-channel { 203 | margin-right: 10px; 204 | max-width: 250px; 205 | } 206 | 207 | #channels { 208 | vertical-align: top; 209 | margin-top: -5px; 210 | max-width: 250px; 211 | } 212 | 213 | #search { 214 | vertical-align: top; 215 | margin-top: -5px; 216 | width: 200px; 217 | } 218 | 219 | .panel { 220 | border-color: #337ab7; 221 | box-shadow: none; 222 | border: 1px solid #337ab7; 223 | border-radius: 5px; 224 | margin-bottom: 20px; 225 | } 226 | 227 | .panel-header { 228 | background-image: linear-gradient(to bottom,#337ab7 0,#2e6da4 100%); 229 | color: #fff; 230 | font-size: 18px; 231 | padding: 10px; 232 | } 233 | 234 | .panel-body { 235 | padding: 15px; 236 | padding-top: 0px; 237 | } 238 | 239 | .file-panel { 240 | height: 110px; 241 | } 242 | 243 | #files-parent-table { 244 | table-layout: fixed; 245 | } 246 | 247 | button.button-primary { 248 | background-color: #337ab7; 249 | border-color: #337ab7; 250 | } 251 | 252 | button, .button { 253 | margin-bottom: 0px; 254 | } 255 | 256 | input[type="submit"].button-primary { 257 | background-color: #337ab7; 258 | border-color: #337ab7; 259 | } 260 | 261 | input[type="submit"].button-primary:hover { 262 | background-color: #337ab7; 263 | border-color: #337ab7; 264 | } 265 | 266 | a { 267 | color: #337ab7; 268 | } 269 | 270 | select { 271 | height: 35px; 272 | } 273 | 274 | .users-panel { 275 | min-height: 220px; 276 | overflow: hidden; 277 | } 278 | 279 | .send-message-panel { 280 | min-height: 220px; 281 | } 282 | 283 | .users-panel-body { 284 | overflow-y: auto; 285 | max-height: 160px; 286 | } 287 | 288 | #new-message-label { 289 | margin-top: 5px; 290 | } 291 | 292 | textarea { 293 | margin-bottom: 10px; 294 | } 295 | 296 | form { 297 | margin-bottom: 0px; 298 | } 299 | 300 | .loading { 301 | border: 4px solid #FFF; 302 | border-top-color: transparent; 303 | border-left-color: transparent; 304 | width: 20px; 305 | height: 20px; 306 | //opacity: 0.8; 307 | border-radius: 50%; 308 | animation: loading 0.7s infinite linear; 309 | -webkit-animation: loading 0.7s infinite linear; 310 | } 311 | 312 | .message-panel-collapse { 313 | height: inherit; 314 | } 315 | 316 | .message-panel-body-collapse { 317 | display: none; 318 | } 319 | 320 | .users-panel-collapse { 321 | height: inherit; 322 | } 323 | 324 | .users-panel-body-collapse { 325 | display: none; 326 | } 327 | 328 | #users-expand { 329 | cursor: pointer; 330 | } 331 | 332 | #message-expand { 333 | cursor: pointer; 334 | } 335 | 336 | th, td{ padding-right: 10px; padding-left: 10px; } 337 | .message td:nth-child(1), 338 | .message th:nth-child(1){ width: 10%; } 339 | .message td:nth-child(2), 340 | .message th:nth-child(2){ width: 50%; } 341 | .message td:nth-child(3), 342 | .message th:nth-child(3){ width: 12%; } 343 | .message td:nth-child(4), 344 | .message th:nth-child(4){ width: 16%; } 345 | .message td:nth-child(5), 346 | .message th:nth-child(5){ width: 12%; } 347 | .message td { word-wrap: break-word; } 348 | 349 | @media (max-width: 1000px) { 350 | .users-panel { 351 | height: 260px; 352 | } 353 | 354 | .send-message-panel { 355 | height: 260px; 356 | } 357 | 358 | .users-panel-body { 359 | max-height: 200px; 360 | } 361 | 362 | .delete-button { 363 | display: none; 364 | } 365 | } 366 | 367 | @media (max-width: 820px) { 368 | .has-docked-nav .navbar > .container, 369 | .container{ width: 100%; padding: 0 10px; } 370 | .logout .navbar-link{ margin-right: 0; } 371 | th, td{ padding-right: 10px; padding-left: 10px; } 372 | } 373 | @media (max-width: 767px) { 374 | th, td{ padding-right: 4px; padding-left: 4px; } 375 | 376 | th.col_node, td.col_node, th.col_channel, td.col_channel, th.col_time, td.col_time { 377 | display:none; 378 | width:0; 379 | height:0; 380 | opacity:0; 381 | visibility: collapse; 382 | } 383 | 384 | .message td:nth-child(1), 385 | .message th:nth-child(1){ width: 20%; } 386 | .message td:nth-child(2), 387 | .message th:nth-child(2){ width: 55%; } 388 | .message td:nth-child(3), 389 | .message th:nth-child(3){ width: 25%; } 390 | .message td:nth-child(4), 391 | .message th:nth-child(4){ width: 0%; } 392 | 393 | body { 394 | font-size: 1.3em; 395 | } 396 | 397 | .users-panel { 398 | height: 260px; 399 | } 400 | 401 | .users-panel-body { 402 | max-height: 200px; 403 | } 404 | 405 | .send-message-panel { 406 | height: 260px; 407 | } 408 | } 409 | 410 | @media (max-width: 600px) { 411 | #search-span { 412 | display: none; 413 | } 414 | 415 | #files-parent-table { 416 | font-size: 1.0em; 417 | } 418 | 419 | #files-parent-table td:first-child { 420 | width: 80%; 421 | } 422 | } 423 | 424 | @media (max-width: 460px) { 425 | #files-parent-table { 426 | table-layout: auto; 427 | font-size: 1.0em; 428 | } 429 | } 430 | 431 | @media (min-width: 400px) { 432 | th, td { 433 | padding-top: 5px; 434 | padding-right: 15px; 435 | } 436 | 437 | body { 438 | font-size: 1.5em; 439 | } 440 | .logout { 441 | float: right; 442 | } 443 | .navbar-link { 444 | font-size: 13px; 445 | text-transform: uppercase; 446 | font-weight: 600; 447 | letter-spacing: .2rem; 448 | margin-right: 35px; 449 | text-decoration: none; 450 | line-height: 6.5rem; 451 | color: #222; 452 | } 453 | .last-updated { 454 | /*float: right;*/ 455 | margin-top: 12px; 456 | } 457 | } 458 | 459 | .col_time { 460 | width: 70px; 461 | max-width: 70px; 462 | } 463 | 464 | @keyframes loading { 465 | from { 466 | transform: rotate(0deg); 467 | } 468 | to { 469 | transform: rotate(360deg); 470 | } 471 | } 472 | @-webkit-keyframes loading { 473 | from { 474 | -webkit-transform: rotate(0deg); 475 | } 476 | to { 477 | -webkit-transform: rotate(360deg); 478 | } 479 | } 480 | --------------------------------------------------------------------------------