diff --git a/.github/ISSUE_TEMPLATE/Bug Report.yml b/.github/ISSUE_TEMPLATE/Bug Report.yml new file mode 100644 index 0000000..3556625 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Bug Report.yml @@ -0,0 +1,102 @@ +name: Bug Report +description: File a bug for the MeshChat project +title: "[Bug]: " +labels: + - bug + - needs triage +assignees: + - hickey +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to create a bug report. Please + attempt to fill in as much information as you are able to. + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: version + attributes: + label: Version + description: Version of MeshChat? + options: + - v1.x + - v2.0 - v2.8 + - v2.9 + - v2.10 + - development build (include version in what happened) + default: 0 + validations: + required: true + - type: dropdown + id: system_type + attributes: + label: System Type + description: What type of system is MeshChat installed on? + options: + - AREDN node + - Linux + - Unknown + default: 0 + validations: + required: true + - type: textarea + id: what-happened + attributes: + label: What happened? + description: Also tell us, what did you expect to happen? + placeholder: | + Describe to the best of your ability what happened or what you + did to trigger the problem. + validations: + required: true + - type: textarea + id: config + attributes: + label: MeshChat configuration + description: | + If you are the admin of the MeshChat instance, it is asked that + you past your MeshChat configuration file between the back ticks + to aid in troubleshooting. + value: | + ``` + + ``` + - type: dropdown + id: connection_type + attributes: + label: Connection type + multiple: false + description: | + How is the node that is running the MeshChat instance connected? + If you know the mesh network that the node is connected to please + indicate the name of the mesh network below in the node name field. + options: + - Non-connected mesh network + - Mesh network connected through IP tunnel + - Mesh network connected through a supernode + - I don't know + - type: input + id: node_name + attributes: + label: Node name + description: Please specify the node name where MeshChat runs. + - type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - Brave + - Vivialdi + - Other diff --git a/.github/ISSUE_TEMPLATE/Feature Request.yml b/.github/ISSUE_TEMPLATE/Feature Request.yml new file mode 100644 index 0000000..eb4ab2e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/Feature Request.yml @@ -0,0 +1,46 @@ +name: Feature Request +description: Looking to add an enhancement to the MeshChat project +title: "[Feature]: " +labels: + - enhancement +assignees: + - hickey +body: + - type: markdown + attributes: + value: | + Thank you for taking the time to let us know about your idea. Please + attempt to fill in as much information as you are able to. + - type: input + id: contact + attributes: + label: Contact Details + description: How can we get in touch with you if we need more info? + placeholder: ex. email@example.com + validations: + required: false + - type: dropdown + id: enhancement_type + attributes: + label: Enhancement Type + description: What sort of enhancement is this? + options: + - Graphical interface + - Message formatting + - File sharing + - API improvements + - Documentation + - Installation method + - Other + default: 0 + validations: + required: true + - type: textarea + id: description + attributes: + label: What is your idea or what can be improved? + description: Please be descriptive so that we can better understand the enhancement. + placeholder: Tell us your idea. + validations: + required: true + diff --git a/.github/workflows/build-packages.yaml b/.github/workflows/build-packages.yaml new file mode 100644 index 0000000..2a0c345 --- /dev/null +++ b/.github/workflows/build-packages.yaml @@ -0,0 +1,36 @@ +name: Build MeshChat Packages +on: push + +jobs: + calculate-version: + runs-on: ubuntu-latest + outputs: + build_version: ${{ steps.build-version-slug.outputs.build_version }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: build-version-slug + run: | + date=$(date +%Y%m%d) + branch="${GITHUB_REF_NAME}" + commit=$(git rev-parse --short HEAD) + version="${date}-${branch}-${commit}" + + echo "build_version=$version" >> $GITHUB_OUTPUT + + build-meshchat-package: + needs: calculate-version + uses: + ./.github/workflows/workflow-meshchat-package.yaml + with: + build_version: ${{ needs.calculate-version.outputs.build_version }} + build_dir: package/meshchat-ipkg + + build-meshchat-api-package: + needs: calculate-version + uses: + ./.github/workflows/workflow-meshchat-api-package.yaml + with: + build_version: ${{ needs.calculate-version.outputs.build_version }} + build_dir: package/meshchat-ipkg diff --git a/.github/workflows/publish-docs.yaml b/.github/workflows/publish-docs.yaml new file mode 100644 index 0000000..1c1eb52 --- /dev/null +++ b/.github/workflows/publish-docs.yaml @@ -0,0 +1,64 @@ +name: Publish MeshChat Documentation +on: + workflow_call: + inputs: + build_version: + required: true + type: string + +jobs: + build: + runs-on: ubuntu-latest + container: + image: jtackaberry/luadox:latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: release + - run: luadox -c luadox.conf + - name: Fix permissions + run: | + chmod -c -R +rX "_site/" | while read line; do + echo "::warning title=Invalid file permissions automatically fixed::$line" + done + - name: Update version strings + run: | + find docs -type f --exec sed -i "s/%VERSION%/${{ inputs.build_version }}/" {} \; + run: | + echo ::group::Archive artifact + tar -C "_site" \ + -cvf "$RUNNER_TEMP/artifact.tar" \ + --exclude=.git \ + --exclude=.github \ + . + echo ::endgroup:: + - name: Upload artifact + id: upload-artifact + uses: actions/upload-artifact@v4 + with: + name: github-pages + path: ${{ runner.temp }}/artifact.tar + retention-days: 1 + if-no-files-found: error + + # Deploy job + deploy: + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + # Specify runner + deployment step + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action diff --git a/.github/workflows/release-meshchat.yaml b/.github/workflows/release-meshchat.yaml index 0da192d..0c4b2ee 100644 --- a/.github/workflows/release-meshchat.yaml +++ b/.github/workflows/release-meshchat.yaml @@ -1,8 +1,5 @@ name: Release MeshChat Package -on: - pull_request: - types: [closed] - branches: [release] +on: workflow_dispatch jobs: create-release: @@ -17,6 +14,7 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + ref: release - name: git config run: | git config user.name "${GITHUB_ACTOR}" @@ -32,15 +30,16 @@ jobs: build-meshchat-package: needs: create-release uses: - ./.github/workflows/build-meshchat-package.yaml + ./.github/workflows/workflow-meshchat-package.yaml with: + ref: release build_version: ${{ needs.create-release.outputs.build_version }} build_dir: package/meshchat-ipkg build-meshchat-api-package: needs: create-release uses: - ./.github/workflows/build-meshchat-api-package.yaml + ./.github/workflows/workflow-meshchat-api-package.yaml with: build_version: ${{ needs.create-release.outputs.build_version }} build_dir: package/meshchat-ipkg @@ -70,3 +69,15 @@ jobs: done env: GITHUB_TOKEN: ${{ secrets.RELEASE_IT_TOKEN }} + + update-documentation: + runs-on: ubuntu-latest + needs: create-release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + - uses: ./.github/workflows/publish-docs.yaml + with: + build_version: ${{ needs.create-release.outputs.build_version }} diff --git a/.github/workflows/workflow-meshchat-api-package.yaml b/.github/workflows/workflow-meshchat-api-package.yaml new file mode 100644 index 0000000..68b88cb --- /dev/null +++ b/.github/workflows/workflow-meshchat-api-package.yaml @@ -0,0 +1,36 @@ +name: Build MeshChat API Package +on: + workflow_call: + inputs: + build_version: + required: true + type: string + build_dir: + required: true + type: string + ref: + required: false + type: string + default: ${{ github.ref_name }} + +jobs: + create-meshchat-api-package: + runs-on: ubuntu-latest + # container: + # image: registry.gitlab.com/wt0f/gitlab-runner-images/shell:latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.ref }} + - run: echo ${{ inputs.build_version }} > VERSION + - run: package/populate-meshchat-api-fs.sh ${{ inputs.build_dir }} + - run: package/update-version.sh ${{ inputs.build_dir }} + - run: package/ipk-build.sh ${{ inputs.build_dir }} + - id: detect-package-file + run: echo "file=$(ls -1 meshchat-api_*.ipk)" >> $GITHUB_OUTPUT + - run: echo "${{ steps.detect-package-file.outputs.file }}" + - uses: actions/upload-artifact@v4 + with: + name: ${{ steps.detect-package-file.outputs.file }} + path: ${{ steps.detect-package-file.outputs.file }} diff --git a/.github/workflows/workflow-meshchat-package.yaml b/.github/workflows/workflow-meshchat-package.yaml new file mode 100644 index 0000000..bee92b0 --- /dev/null +++ b/.github/workflows/workflow-meshchat-package.yaml @@ -0,0 +1,41 @@ +name: Build MeshChat Package +on: + workflow_call: + inputs: + build_version: + required: true + type: string + build_dir: + required: true + type: string + ref: + required: false + type: string + default: ${{ github.ref_name }} + +jobs: + create-meshchat-package: + runs-on: ubuntu-latest + # container: + # image: registry.gitlab.com/wt0f/gitlab-runner-images/shell:latest + outputs: + package_file: ${{ steps.detect-package-file.outputs.file }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.ref }} + # - run: info "Populating the filesystem with MeshChat files" + - run: echo ${{ inputs.build_version }} > VERSION + - run: package/populate-meshchat-fs.sh ${{ inputs.build_dir }} + # - run: info "Updating version numbers to " + - run: package/update-version.sh ${{ inputs.build_dir }} + # - run: info "Packing up MeshChat files" + - run: package/ipk-build.sh ${{ inputs.build_dir }} + - id: detect-package-file + run: echo "file=$(ls -1 meshchat_*.ipk)" >> $GITHUB_OUTPUT + - run: echo "${{ steps.detect-package-file.outputs.file }}" + - uses: actions/upload-artifact@v4 + with: + name: ${{ steps.detect-package-file.outputs.file }} + path: ${{ steps.detect-package-file.outputs.file }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54c4b32 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +docs/.markupserve_index diff --git a/.release-it.yaml b/.release-it.yaml new file mode 100644 index 0000000..66b6baa --- /dev/null +++ b/.release-it.yaml @@ -0,0 +1,31 @@ +git: + commit: true + commitMessage: "chore(release): ${version}" + commitArgs: "" + tag: true + tagName: "v${version}" + tagAnnotation: "Automated release: ${version}" + push: true + requireBranch: release + requireCommits: true + changelog: "npx auto-changelog --stdout --commit-limit false" + +github: + release: true + releaseName: "v${version}" + +npm: + publish: false + +plugins: + "@release-it/conventional-changelog": + infile: CHANGELOG.md + preset: + name: conventionalcommits + types: + - type: feat + section: Features + - type: fix + section: Bug Fixes + - tyep: docs + section: Documentation diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 2595aaf..64ac965 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,16 @@ -# meshchat -meshchat for AREDN (in Lua) +# MeshChat + +MeshChat for AREDN (in Lua). MeshChat has become the defacto standard +chat application for AREDN networks. A number of features make it easy +to implement and use: + +* Installable on AREDN firmware and most any Linux distribution +* Automatic synchronization between nodes using the same service name +* No account creation necessary--users access using call sign +* Simple user interface + +If you are looking for a feature to be implemented or find a bug, please +be sure to [create an issue](https://github.com/hickey/meshchat/issues/new) +in the project so that it can be prioritized. + -Based on the Perl version https://github.com/tpaskett/meshchat diff --git a/apionly/data/www/cgi-bin/meshchat b/api/meshchat similarity index 100% rename from apionly/data/www/cgi-bin/meshchat rename to api/meshchat diff --git a/build b/build deleted file mode 100755 index 96b6808..0000000 --- a/build +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/bash - -rm -rf *.ipk *.deb -find . -name '*~' -delete - -### Build AREDN package -export COPYFILE_DISABLE=1 -export VERSION=2.9 - -# Main -rm -rf data.tar.gz control.tar.gz -cd src/data -sed -i "s/^app_version.*$/app_version = \"${VERSION}\"/" www/cgi-bin/meshchatconfig.lua -tar cf ../../data.tar `find . -type f | grep -v DS_Store | grep -v .pl | grep -v .pm` -cd ../control -sed -i "s/^Version: .*$/Version: ${VERSION}/" control -tar cfz ../../control.tar.gz . -cd ../.. -gzip data.tar -COPYFILE_DISABLE=1 tar cfz meshchat_${VERSION}_all.ipk control.tar.gz data.tar.gz debian-binary - -# API-only -rm -rf data.tar.gz control.tar.gz -cd apionly/data -tar cf ../../data.tar `find . -type f | grep -v DS_Store | grep -v .pl | grep -v .pm` -cd ../control -sed -i "s/^Version: .*$/Version: ${VERSION}/" control -tar cfz ../../control.tar.gz . -cd ../.. -gzip data.tar -COPYFILE_DISABLE=1 tar cfz meshchat-api_${VERSION}_all.ipk control.tar.gz data.tar.gz debian-binary - -rm -rf data.tar.gz control.tar.gz *.deb diff --git a/debian-binary b/debian-binary deleted file mode 100644 index cd5ac03..0000000 --- a/debian-binary +++ /dev/null @@ -1 +0,0 @@ -2.0 diff --git a/docs/History.md b/docs/History.md new file mode 100644 index 0000000..9b22c3c --- /dev/null +++ b/docs/History.md @@ -0,0 +1,38 @@ +# History of MeshChat + +This is the history of the various MeshChat versions that have existed--at +least to the best of my knowledge. + +## MeshChat v0.4 - v1.02 + +This was the original version of MeshChat written by Trevor Paskett (K7FPV) +around 2015. It was written in Perl and worked well on the limited resources +of the AREDN nodes. Around 2018 Trevor was not able to or not interested +in supporting MeshChat any longer, it is unclear which but the project +became stagnant at version v1.01 in August of 2018. There was a final +release of v1.02 in September 2022 that mostly added a few patches and +support for Debian Stretch. + +The K7FPV code base still exists at https://github.com/tpaskett/meshchat. + +In addition Trevor wrote a good amount of documentation for his versions +which is still pretty well covers the current versions of MeshChat. +The documentation can be found over at his blog, https://github.com/tpaskett/meshchat. + +## MeshChat v2.0 - v2.10 + +When AREDN firmware v3.22.6.0 was released in June 2022, the AREDN development +team stopped including Perl in the distribution in favor of LUA. In preparation +of this change Tim Wilkinson (KN6PLV) started rewriting MeshChat in LUA +March 2022 with the first release of the new code base in April 2022. The +new MeshChat code continued to receive bug fixes for a year. At which +time Tim's involvement on the AREDN development team prevented him from +continuing to maintain MeshChat. + +## Future of MeshChat + +That brings the story upto the current time, September 2023, where I, +Gerard Hickey (WT0F), have started to be the maintainer of the MeshChat +code base. There has already been work to restructure the repository to +make working with the code more effective and to automatically build +packages when a release occurs. diff --git a/docs/Install.md b/docs/Install.md new file mode 100644 index 0000000..8f3bf3e --- /dev/null +++ b/docs/Install.md @@ -0,0 +1,22 @@ +# Installing MeshChat + +MeshChat is distributed as an Itsy package (IPK file) to be installed on an +AREDN node. This is the simplest way to install MeshChat. + +Simply download the MeshChat package to your compute and then access the +Administration panel in the AREDN's node setup. Under Package Management +you will have the option to upload a package. Once uploaded the MeshChat +system will be started within a couple of seconds. + +Usually there is not really any configuration that needs to be done, but +review of the [configuration settings](../module/meshchatconfig.html) is +suggested. To make any configuration changes one needs to log into the +node using SSH and edit the file `/www/cgi-bin/meshchatconfig.lua`. + +## Installing MeshChat on Linux + +The current distribution of MeshChat does not currently support Linux. In +order to run MeshChat on a Linux machine, one needs to download MeshChat +v1.0.2 and install it on the Linux machine. Once installed, the configuration +need to be updated to set the `api_host` setting to the hostname or IP +of an AREDN node that has the MeshChat API package installed. diff --git a/docs/Troubleshooting.md b/docs/Troubleshooting.md new file mode 100644 index 0000000..2c6bdd1 --- /dev/null +++ b/docs/Troubleshooting.md @@ -0,0 +1,57 @@ +# Troubleshooting + +This is a "living" document. It is attempted to keep it up to date with +any new problems and troubleshooting techniques. If you find something +missing, please create an [Issue](https://github.com/hickey/meshchat/issues/new/choose) +do describe what problem or issue is missing. Better yet is to fork the +MeshChat repository, update the documentation in the forked repository +and then generate a PR back to the official MeshChat repository. Your +efforts will be greatly appreciated. + +It is important to realize that MeshChat is effectively two separate +programs: one that runs in your browser (the frontend code) and one that +runs on the AREDN node (the backend code or API). While it may not be +obvious which piece of code is having the problem, it generally can be +broken down as if there is an issue with the format of a message or it +being displayed in the browser then the frontend code should be investigated. +Otherwise the API should be investigated. + +## Installation Issues + +There is a known issue that if an older AREDN firmware is being upgraded, +any additional packages will need to be reinstalled after the node has +completed the firmware upgrade. This should not be the case for AREDN +firmware 3.23.8.0 or greater. + +If it appears that the installation of the package did not completely +install or is not fully functional, check the node to determine how much +disk space is available. Generally one should plan on a minimum of 100 KB +of disk space for MeshChat to operate. + +Package installation failures also generally have an error message displayed +above the upload button when there is a failure. This can help indicate +what the failure type was, so it should be reported back as a project +issue using the link above. + +## Message Synchronization Issues + +In order for messages to be synchronized between MeshChat instances, the +`meshchatsync` process needs to be running. Log into the node and execute +`ps | grep meshchatsync` to see if the process exists. If it is not +running, then one can start it with executing `/usr/local/bin/meshchatsync`. +Doing so will keep the process attached to the current terminal and any +error output will be displayed in the terminal. Once the terminal is +exited, the `meshchatsync` process will terminate. So after determining +that there are no errors being generated, it is best to reboot the node. +This will allow `meshchatsync` to startup normally with no manual +intervention. + +If it appears that `meshchatsync` is operating correctly, then the next +item to check is that the message database exists and messages are being +written to it. On an AREDN node, the message database is normally located +in `/tmp/meshchat`. Check for a `messages.`. If the message +database does exist, post a new message in the MeshChat instance on the +node and insure that the message gets written to the message database. + +Also insure that the message database has write permissions on the file. + diff --git a/luadox.conf b/luadox.conf new file mode 100644 index 0000000..c5e0862 --- /dev/null +++ b/luadox.conf @@ -0,0 +1,29 @@ +[project] +# Project name that is displayed on the top bar of each page +name = MeshChat +# HTML title that is appended to every page. If not defined, name is used. +title = MeshChat (master) +# A list of files or directories for LuaDox to parse. Globs are supported. +# This can be spread across multiple lines if you want, as long as the +# other lines are indented. +files = meshchat* +#files = /data/src/data/www/cgi-bin/meshchat.lua /data/src/data/www/cgi-bin/meshchatlib.lua +# The directory containing the rendered output files, which will be created +# if necessary. +outdir = _site +# Path to a custom css file that will be included on every page. This will +# be copied into the outdir. +# css = custom.css +# Path to a custom favicon. This will be copied into the outdir. +# favicon = img/favicon.png +# If require()d files discovered in source should also be parsed. +follow = false +# Character encoding for input files, which defaults to the current system +# locale. Output files are always utf8. +encoding = utf8 + +[manual] +index = README.md +history = docs/History.md +install = docs/Install.md +troubleshoot = docs/Troubleshooting.md diff --git a/src/data/www/cgi-bin/meshchat b/meshchat similarity index 70% rename from src/data/www/cgi-bin/meshchat rename to meshchat index 7c0fd52..9a3c507 100755 --- a/src/data/www/cgi-bin/meshchat +++ b/meshchat @@ -38,20 +38,23 @@ package.path = package.path .. ";/www/cgi-bin/?.lua" require('luci.http') -require("luci.jsonc") +local json = require("luci.jsonc") require("nixio") require("meshchatconfig") require("meshchatlib") +--- +-- @module meshchat + local query = {} local uploadfilename if os.getenv("QUERY_STRING") ~= "" or os.getenv("REQUEST_METHOD") == "POST" then local request = luci.http.Request(nixio.getenv(), - function() - local v = io.read(1024) - if not v then - io.close() - end + function() + local v = io.read(1024) + if not v then + io.close() + end return v end ) @@ -76,24 +79,88 @@ if os.getenv("QUERY_STRING") ~= "" or os.getenv("REQUEST_METHOD") == "POST" then query = request:formvalue() end +--- Return an error page to a browser. +-- @tparam string msg Error message to be displayed +-- function error(msg) print("Content-type: text/plain\r") print("\r") print(msg) end +--- @section API + +--- Returns a JSON document with basic node configuration. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `config` | +-- +-- ## API Response +-- +-- @example +-- { +-- "version": "meshchat_version", +-- "node": "node_name", +-- "zone": "meshchat_zone_name" +-- } +-- function config() print("Content-type: application/json\r") print("\r") - print(string.format([[{"version":"%s","node":"%s","zone":"%s"}]], app_version, node_name(), zone_name())) + + local settings = { + version = app_version, + protocol_verison = protocol_version, + node = node_name(), + zone = zone_name(), + default_channel = default_channel, + debug = debug, + } + + print(json.stringify(settings)) end +--- Send a message to the MeshChat instance. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `send_message` | +-- | message | yes | Message body | +-- | call_sign | yes | Call sign of the sender | +-- | channel | no | Channel name to post message | +-- | epoch | no | Timestamp specified as unixtime | +-- +-- @note message +-- Needs to have newslines and double quotes escaped. +-- +-- @note channel +-- If not specified or set to empty string will post message to +-- `Everything` channel +-- +-- @note epoch +-- If not specified, the current time on the MeshChat server will +-- be used. +-- +-- ## API Response +-- +-- On a successful entry of the message into the database a success JSON +-- document will be returned. +-- +-- @example +-- { +-- "status": 200, +-- "response": "OK" +-- } +-- function send_message() print("Content-type: application/json\r") print("\r") local message = query.message:gsub("\n", "\\n"):gsub('"', '\\"'):gsub("\t", " ") - + local id = query.id or hash(); local epoch = os.time() if tonumber(query.epoch) > epoch then epoch = query.epoch @@ -104,9 +171,10 @@ function send_message() local f = io.open(messages_db_file, "a") if not f then release_lock() + -- TODO return a proper error code on failure die("Cannot send message") end - f:write(hash() .. "\t" .. epoch .. "\t" .. message .. "\t" .. query.call_sign .. "\t" .. node_name() .. "\t" .. platform .. "\t" .. query.channel .. "\n") + f:write(id .. "\t" .. epoch .. "\t" .. message .. "\t" .. query.call_sign .. "\t" .. node_name() .. "\t" .. platform .. "\t" .. query.channel .. "\n") f:close() sort_and_trim_db() @@ -117,6 +185,29 @@ function send_message() print([[{"status":200, "response":"OK"}]]) end +--- Return a list of message stored on the MeshChat instance. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `messages` | +-- | call_sign | no | Call sign of the requester | +-- | epoch | no | Timestamp specified as unixtime | +-- | id | no | Generated MeshChat ID | +-- +-- ## API Response +-- +-- @example +-- { +-- "id": "id_str", +-- "epoch": epoch_time, +-- "message": "message_text", +-- "call_sign": "sending_call_sign", +-- "node": "originating_node", +-- "platform": "originating_node_platform", +-- "channel": "channel" +-- } +-- function messages() print("Content-type: application/json\r") @@ -133,6 +224,7 @@ function messages() local node = node_name() + -- read in message DB and parse the contents local messages = {} for line in io.lines(messages_db_file) do @@ -152,6 +244,8 @@ function messages() if tonumber(query.epoch) and query.call_sign then local users = {} + + -- read the users status file if nixio.fs.stat(local_users_status_file) then for line in io.lines(local_users_status_file) do @@ -162,11 +256,14 @@ function messages() end end + -- set the timestamp local epoch = os.time() if tonumber(query.epoch) > epoch then epoch = query.epoch end + -- rewrite user status file updating the timestamp for requesting call sign + -- query.id is the meshchat_id local f = io.open(local_users_status_file, "w") if f then local found_user = false @@ -188,13 +285,29 @@ function messages() release_lock() + -- order messages according to time table.sort(messages, function(a, b) return a.epoch > b.epoch end) - output:write(luci.jsonc.stringify(messages)) + output:write(json.stringify(messages)) output:flush() end +--- Return a JSON document describing the sync status. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `sync_status` | +-- +-- ## API Response +-- +-- @example +-- { +-- "node": "node_name", +-- "epoch": sync_time +-- } +-- function sync_status() print("Content-type: application/json\r") print("\r") @@ -217,9 +330,10 @@ function sync_status() table.sort(status, function(a, b) return a.epoch > b.epoch end) - print(luci.jsonc.stringify(status)) + print(json.stringify(status)) end +--- Return a list of messages as text. function messages_raw() get_lock() @@ -242,6 +356,7 @@ function messages_raw() end end +--- Return the current MD5 has of the messages database. function messages_md5() get_lock() @@ -254,6 +369,7 @@ function messages_md5() print(md5) end +--- Package the raw messages as the messages.txt file. function messages_download() get_lock() @@ -277,6 +393,7 @@ function messages_download() end end +--- Return the list of users as raw text. function users_raw() get_lock() @@ -299,6 +416,24 @@ function users_raw() end end +--- Return a JSON document describing the logged in users. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `users` | +-- +-- ## API Response +-- +-- @example +-- { +-- "id": "id_str", +-- "epoch": epoch_time, +-- "call_sign": "sender_call_sign', +-- "node": "originating_node", +-- "platform": "originating_platform", +-- } +-- function users() print("Content-type: application/json\r") print("\r") @@ -337,9 +472,10 @@ function users() table.sort(users, function(a, b) return a.epoch > b.epoch end) - print(luci.jsonc.stringify(users)) + print(json.stringify(users)) end +--- Return a list of files as plain text. function local_files_raw() get_lock() @@ -352,7 +488,7 @@ function local_files_raw() for file in nixio.fs.dir(local_files_dir) do local stat = nixio.fs.stat(local_files_dir .. "/" .. file) - f:write(file .. "\t" .. name .. "\t" .. stat.size .. "\t" .. stat.mtime .. "\t" .. platform .. "\n") + f:write(file .. "\t" .. name .. "\t" .. stat.size .. "\t" .. stat.mtime .. platform .. "\n") end f:close() @@ -372,6 +508,19 @@ function local_files_raw() nixio.fs.remove(tmp_file) end +--- Return a specified file as a download. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `file_download` | +-- | file | yes | Name of file to downlaod | +-- +-- ## API Response +-- +-- Returns a page as an octet-stream that is tagged as an attachment +-- to cause the browser to receive the file as a download. +-- function file_download() local file = query.file local file_path = local_files_dir .. "/" .. file @@ -399,6 +548,24 @@ function file_download() end end +--- Return a JSON document describing the list of files. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `files` | +-- +-- ## API Response +-- +-- @example +-- { +-- "file": "filename", +-- "epoch": modification_time, +-- "size": file_size_in_bytes, +-- "node": "originating_node", +-- "platform": "originating_platform" +-- } +-- function files() print("Content-type: application/json\r") print("\r") @@ -445,12 +612,13 @@ function files() table.sort(files, function(a, b) return a.epoch > b.epoch end) - print(luci.jsonc.stringify({ + print(json.stringify({ stats = stats, files = files })) end +--- Delete the specified file. function delete_file() nixio.fs.remove(local_files_dir .. "/" .. query.file) print("Content-type: application/json\r") @@ -458,16 +626,18 @@ function delete_file() print([[{"status":200, "response":"OK"}]]) end +--- Return the current version string for the messages database. function messages_version() print("Content-type: text/plain\r") print("\r") print(get_messages_db_version()) end +--- Return a JSON document of the messages database. function messages_version_ui() print("Content-type: application/json\r") print("\r") - + print(string.format([[{"messages_version":%s}]], get_messages_db_version())) get_lock() @@ -487,6 +657,7 @@ function messages_version_ui() epoch = query.epoch end + -- TODO refactor here and messages function into a single code block local f = io.open(local_users_status_file, "w") if f then local found_user = false @@ -508,10 +679,26 @@ function messages_version_ui() release_lock() end +--- Return a JSON document describing all the hosts. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `hosts` | +-- +-- ## API Response +-- +-- @example +-- { +-- "ip": "ip_address", +-- "hostname": "hostname", +-- "node": "node_name" +-- } +-- function hosts() print("Content-type: application/json\r") print("\r") - + local node = node_name() local hosts = {} for line in io.lines("/var/dhcp.leases") @@ -556,13 +743,14 @@ function hosts() table.sort(hosts, function(a, b) return a.hostname < b.hostname end) - print(luci.jsonc.stringify(hosts)) + print(json.stringify(hosts)) end +--- Return a list of hosts as plain text. function hosts_raw() print("Content-type: application/json\r") print("\r") - + local hosts = {} for line in io.lines("/var/dhcp.leases") do @@ -589,6 +777,7 @@ function hosts_raw() end end +--- Store a file into the file directory. function upload_file() local new_file_size = nixio.fs.stat(tmp_upload_dir .. "/file").size @@ -615,6 +804,22 @@ function upload_file() end end +--- Return a list of nodes running MeshChat as text. +-- +-- ## API Parameters +-- | Parameter | Required | Description | +-- |-----------|----------|------------------------------------------| +-- | action | yes | Must be set to `meshchat_nodes` | +-- | zone_name | yes | MeshChat zone name | +-- +-- ## API Response +-- +-- The list of nodes and ports seperated by a tab. +-- +-- @example +-- node1 8080 +-- node2 8080 +-- function meshchat_nodes() print("Content-type: text/plain\r") print("\r") @@ -629,6 +834,11 @@ function meshchat_nodes() end end +--- Return a JSON document of the action log. +-- +-- Currently this call returns an empty list. In the future it will +-- return a list of action log events. +-- function action_log() print("Content-type: application/json\r") print("\r") diff --git a/src/data/www/cgi-bin/meshchatconfig.lua b/meshchatconfig.lua similarity index 81% rename from src/data/www/cgi-bin/meshchatconfig.lua rename to meshchatconfig.lua index 6ca23b3..b91e5ab 100755 --- a/src/data/www/cgi-bin/meshchatconfig.lua +++ b/meshchatconfig.lua @@ -34,8 +34,18 @@ --]] +--- +-- @module meshchatconfig +-- @section MeshChat Configuration + +--- Base directory to store all MeshChat generated files +-- @type string meshchat_path = "/tmp/meshchat" +--- Maximum number of messages in the database +-- @type int max_messages_db_size = 500 +--- Maximum amount of filesystem space for storing files +-- @type int max_file_storage = 512 * 1024 lock_file = meshchat_path .. "/lock" messages_db_file = meshchat_path .. "/messages" @@ -47,14 +57,23 @@ remote_files_file = meshchat_path .. "/files_remote" messages_version_file = meshchat_path .. "/messages_version" local_files_dir = meshchat_path .. "/files" tmp_upload_dir = "/tmp/web/upload" +--- How often to check for new messages +-- @type int poll_interval = 10 non_meshchat_poll_interval = 600 valid_future_message_time = 30 * 24 * 60 * 60 connect_timeout = 5 speed_time = 10 speed_limit = 1000 +--- Type of node that MeshChat is installed on ("node" or "pi") +-- @type string platform = "node" +--- Turn debug message on +-- @type bool debug = 0 extra_nodes = {} +--- MeshChat protocol version +-- @type string protocol_version = "1.02" -app_version = "2.9" +app_version = "master" +default_channel = "chat" diff --git a/src/data/www/cgi-bin/meshchatlib.lua b/meshchatlib.lua similarity index 81% rename from src/data/www/cgi-bin/meshchatlib.lua rename to meshchatlib.lua index 33fde9d..f409ea2 100755 --- a/src/data/www/cgi-bin/meshchatlib.lua +++ b/meshchatlib.lua @@ -2,7 +2,7 @@ Part of AREDN -- Used for creating Amateur Radio Emergency Data Networks Copyright (C) 2022 Tim Wilkinson - Base on code (C) Trevor Paskett (see https://github.com/tpaskett) + Base on code (C) Trevor Paskett (see https://github.com/tpaskett) See Contributors file for additional contributors This program is free software: you can redistribute it and/or modify @@ -37,10 +37,21 @@ require("nixio") require("uci") +--- @module meshchatlib + +--- Exit the program with an error message. +-- +-- @tparam string msg Message to display +-- function die(msg) os.exit(-1) end +--- Execute a command and capture the output. +-- +-- @tparam string cmd Command line to execute +-- @treturn string stdout of the command +-- function capture(cmd) local f = io.popen(cmd) if not f then @@ -51,10 +62,23 @@ function capture(cmd) return output end +--- +-- Retrieve the current node name. +-- +-- This function will interogate the UCI settings to retrieve the current +-- node name stored in the `hsmmmesh` settings. +-- +-- @treturn string Name of current node +-- function node_name() return uci.cursor("/etc/local/uci"):get("hsmmmesh", "settings", "node") or "" end +--- +-- Retrieve the current MeshChat zone name that the node is operating under. +-- +-- @treturn string Name of MeshChat zone +-- function zone_name() local dmz_mode = uci.cursor("/etc/config.mesh"):get("aredn", "@dmz[0]", "mode") local servfile = "/etc/config.mesh/_setup.services.nat" @@ -88,6 +112,18 @@ function release_lock() lock_fd:lock("ulock") end +--- Generate the MD5 sum of a file. +-- +-- This under the covers relies on `md5sum` and executes `md5sum` against +-- the specified file. +-- +-- @note +-- There is no checking to determine if `md5sum` is installed or +-- executable. In the future, this may change. +-- +-- @tparam string file Path to file +-- @treturn string Result of `md5sum` of the file +-- function file_md5(file) if not nixio.fs.stat(file) then return "" @@ -123,6 +159,17 @@ function get_messages_version_file() return sum end +--- Generate a unique hash. +-- +-- Combine the current time (epoch time) and a randomly generated number +-- between 0 - 99999 and run through `md5sum` to generate a random hash. +-- +-- @note +-- There is no checking to determine if `md5sum` is installed or +-- executable. In the future, this may change. +-- +-- @treturn string Generated hash value +-- function hash() return capture("echo " .. os.time() .. math.random(99999) .. " | md5sum"):sub(1, 8) end @@ -227,6 +274,12 @@ function node_list() return nodes end +--- +-- Escape percent signs. +-- +-- @tparam string str String to encode +-- @treturn string Encoded string +-- function str_escape(str) return str:gsub("%(", "%%("):gsub("%)", "%%)"):gsub("%%", "%%%%"):gsub("%.", "%%."):gsub("%+", "%%+"):gsub("-", "%%-"):gsub("%*", "%%*"):gsub("%[", "%%["):gsub("%?", "%%?"):gsub("%^", "%%^"):gsub("%$", "%%$") end diff --git a/package/ipk-build.sh b/package/ipk-build.sh new file mode 100755 index 0000000..6de69b5 --- /dev/null +++ b/package/ipk-build.sh @@ -0,0 +1,126 @@ +#!/bin/sh +# ipkg-build -- construct a .ipk from a directory +# Carl Worth +# based on a script by Steve Redler IV, steve@sr-tech.com 5-21-2001 +set -e + +ipkg_extract_value() { + sed -e "s/^[^:]*:[[:space:]]*//" +} + +required_field() { + field=$1 + + value=`grep "^$field:" < $CONTROL/control | ipkg_extract_value` + if [ -z "$value" ]; then + echo "ipkg-build: Error: $CONTROL/control is missing field $field" ; + PKG_ERROR=1 + fi + echo $value +} + +pkg_appears_sane() { + local pkg_dir=$1 + + local owd=`pwd` + cd $pkg_dir + + PKG_ERROR=0 + if [ ! -f "$CONTROL/control" ]; then + echo "ipkg-build: Error: Control file $pkg_dir/$CONTROL/control not found." + cd $owd + return 1 + fi + + pkg=`required_field Package` + version=`required_field Version` + arch=`required_field Architecture` + required_field Maintainer >/dev/null + required_field Description >/dev/null + + if echo $pkg | grep '[^a-z0-9.+-]'; then + echo "ipkg-build: Error: Package name $name contains illegal characters, (other than [a-z0-9.+-])" + PKG_ERROR=1; + fi + + local bad_fields=`sed -ne 's/^\([^[:space:]][^:[:space:]]\+[[:space:]]\+\)[^:].*/\1/p' < $CONTROL/control | sed -e 's/\\n//'` + if [ -n "$bad_fields" ]; then + bad_fields=`echo $bad_fields` + echo "ipkg-build: Error: The following fields in $CONTROL/control are missing a ':'" + echo " $bad_fields" + echo "ipkg-build: This may be due to a missing initial space for a multi-line field value" + PKG_ERROR=1 + fi + + for script in $CONTROL/preinst $CONTROL/postinst $CONTROL/prerm $CONTROL/postrm; do + if [ -f $script -a ! -x $script ]; then + echo "ipkg-build: Error: package script $script is not executable" + PKG_ERROR=1 + fi + done + + if [ -f $CONTROL/conffiles ]; then + for cf in `cat $CONTROL/conffiles`; do + if [ ! -f ./$cf ]; then + echo "ipkg-build: Error: $CONTROL/conffiles mentions conffile $cf which does not exist" + PKG_ERROR=1 + fi + done + fi + + cd $owd + return $PKG_ERROR +} + +### +# ipkg-build "main" +### + +case $# in +1) + dest_dir=. + ;; +2) + dest_dir=$2 + ;; +*) + echo "Usage: ipkg-build []" ; + exit 1 + ;; +esac + +pkg_dir=$1 + +if [ ! -d $pkg_dir ]; then + echo "ipkg-build: Error: Directory $pkg_dir does not exist" + exit 1 +fi + +# CONTROL is second so that it takes precedence +CONTROL= +[ -d $pkg_dir/DEBIAN ] && CONTROL=DEBIAN +[ -d $pkg_dir/CONTROL ] && CONTROL=CONTROL +if [ -z "$CONTROL" ]; then + echo "ipkg-build: Error: Directory $pkg_dir has no CONTROL subdirectory." + exit 1 +fi + +if ! pkg_appears_sane $pkg_dir; then + echo "Please fix the above errors and try again." + exit 1 +fi + +tmp_dir=$dest_dir/IPKG_BUILD.$$ +mkdir $tmp_dir + +tar -C $pkg_dir --exclude=$CONTROL -czf $tmp_dir/data.tar.gz . +tar -C $pkg_dir/$CONTROL -czf $tmp_dir/control.tar.gz . + +echo "2.0" > $tmp_dir/debian-binary + +pkg_file=$dest_dir/${pkg}_${version}_${arch}.ipk +tar -C $tmp_dir -czf $pkg_file debian-binary data.tar.gz control.tar.gz +rm $tmp_dir/debian-binary $tmp_dir/data.tar.gz $tmp_dir/control.tar.gz +rmdir $tmp_dir + +echo "Packaged contents of $pkg_dir into $pkg_file" diff --git a/apionly/control/control b/package/meshchat-api/control similarity index 74% rename from apionly/control/control rename to package/meshchat-api/control index e1a678a..0761bf2 100755 --- a/apionly/control/control +++ b/package/meshchat-api/control @@ -1,8 +1,8 @@ Package: meshchat-api -Version: 2.9 -Depends: +Version: +Depends: lua Provides: -Source: package/meshchat-api +Source: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY Section: net Priority: optional Maintainer: Tim Wilkinson (KN6PLV) and Trevor Paskett (K7FPV) diff --git a/package/meshchat/conffiles b/package/meshchat/conffiles new file mode 100644 index 0000000..d4a7ac7 --- /dev/null +++ b/package/meshchat/conffiles @@ -0,0 +1 @@ +www/cgi-bin/meshchatconfig.lua diff --git a/src/control/control b/package/meshchat/control similarity index 88% rename from src/control/control rename to package/meshchat/control index 6204c69..d78266e 100644 --- a/src/control/control +++ b/package/meshchat/control @@ -1,6 +1,6 @@ Package: meshchat -Version: 2.9 -Depends: curl +Version: +Depends: curl lua Provides: Source: package/meshchat Section: net diff --git a/src/control/postinst b/package/meshchat/postinst similarity index 100% rename from src/control/postinst rename to package/meshchat/postinst diff --git a/src/control/preinst b/package/meshchat/preinst similarity index 100% rename from src/control/preinst rename to package/meshchat/preinst diff --git a/src/control/prerm b/package/meshchat/prerm similarity index 100% rename from src/control/prerm rename to package/meshchat/prerm diff --git a/package/populate-meshchat-api-fs.sh b/package/populate-meshchat-api-fs.sh new file mode 100755 index 0000000..19ec8b5 --- /dev/null +++ b/package/populate-meshchat-api-fs.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +# This script runs from the top of the project directory and +# creates the filesystem image for the MeshChat API package. + +IPK_DIR=$1 + +# Populate the CONTROL portion of the package +mkdir -p $IPK_DIR/CONTROL +cp -p package/meshchat-api/* $IPK_DIR/CONTROL/ +sed -i "s%\$GITHUB_SERVER_URL%$GITHUB_SERVER_URL%" $IPK_DIR/CONTROL/control +sed -i "s%\$GITHUB_REPOSITORY%$GITHUB_REPOSITORY%" $IPK_DIR/CONTROL/control + +# Populate the filesystem image for the package +install -D api/meshchat -m 755 $IPK_DIR/www/cgi-bin/meshchat + diff --git a/package/populate-meshchat-fs.sh b/package/populate-meshchat-fs.sh new file mode 100755 index 0000000..656df9b --- /dev/null +++ b/package/populate-meshchat-fs.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# This script runs from the top of the project directory and +# creates the filesystem image for the MeshChat API package. + +IPK_DIR=$1 + +# Populate the CONTROL portion of the package +mkdir -p $IPK_DIR/CONTROL +cp -p package/meshchat/* $IPK_DIR/CONTROL/ +sed -i "s%\$GITHUB_SERVER_URL%$GITHUB_SERVER_URL%" $IPK_DIR/CONTROL/control +sed -i "s%\$GITHUB_REPOSITORY%$GITHUB_REPOSITORY%" $IPK_DIR/CONTROL/control + +# Populate the filesystem image for the package +install -d $IPK_DIR/www/meshchat +install www/* $IPK_DIR/www/meshchat +install -d $IPK_DIR/www/cgi-bin +install -m 755 meshchat $IPK_DIR/www/cgi-bin +install -m 644 meshchatlib.lua $IPK_DIR/www/cgi-bin +install -m 644 meshchatconfig.lua $IPK_DIR/www/cgi-bin +install -D support/meshchatsync-init.d -m 755 $IPK_DIR/etc/init.d/meshchatsync +install -D support/meshchatsync -m 755 $IPK_DIR/usr/local/bin/meshchatsync diff --git a/package/update-version.sh b/package/update-version.sh new file mode 100755 index 0000000..9e169c8 --- /dev/null +++ b/package/update-version.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +# This script runs from the top of the project directory and +# updates the version number in the control file for the package +# being built. + +IPK_DIR=$1 + +if [[ -f VERSION ]]; then + version=$(cat VERSION) +else + if [[ "${GITHUB_REF_TYPE}" == 'tag' ]]; then + # ideally should only get version tags (i.e. 'v' followed by a number) + if [[ "${GITHUB_REF_NAME}" =~ ^v[0-9].* ]]; then + version="${GITHUB_REF_NAME#v}" + fi + elif [[ -n "${CI_COMMIT_TAG}" ]]; then + # ideally should only get version tags (i.e. 'v' followed by a number) + if [[ "${CI_COMMIT_TAG}" =~ ^v[0-9].* ]]; then + version="${CI_COMMIT_TAG#v}" + fi + else + # branch gets date code-branch_name-commit + date=$(date +%Y%m%d) + branch=$(git rev-parse --abbrev-ref HEAD) + # maybe a detached head, so check common vars for branch name + if [[ -n "${CI_COMMIT_REF_NAME}" ]]; then + branch="${CI_COMMIT_REF_NAME}" + elif [[ -n "${GITHUB_REF_NAME}" ]]; then + branch="${GITHUB_REF_NAME}" + fi + commit=$(git rev-parse --short HEAD) + version="${date}-${branch}-${commit}" + fi +fi + +# write the version to a VERSION file +echo "${version}" > VERSION +echo "Updating code references to version ${version}" + +sed -i "s/^Version:.*/Version: $version/" $IPK_DIR/CONTROL/control + +# Update the version in meshchatconfig.lua if present +if [[ -f $IPK_DIR/www/cgi-bin/meshchatconfig.lua ]]; then + sed -i "s/^app_version.*$/app_version = \"${version}\"/" $IPK_DIR/www/cgi-bin/meshchatconfig.lua +fi diff --git a/src/data/www/meshchat/chat.js b/src/data/www/meshchat/chat.js deleted file mode 100644 index 0e89eea..0000000 --- a/src/data/www/meshchat/chat.js +++ /dev/null @@ -1,394 +0,0 @@ -var last_messages_update = epoch(); -var call_sign = 'NOCALL'; -var meshchat_id; -var peer; -var mediaConnection; -var enable_video = 0; -var messages_updating = false; -var users_updating = false; -var messages = []; -var channel_filter = ''; -var messages_version = 0; -var alert = new Audio('alert.mp3'); -var message_db_version = 0; -var pending_message_db_version = 0; -var search_filter = ''; - -$(function() { - meshchat_init(); -}); - -function monitor_last_update() { - var secs = epoch() - last_messages_update; - $('#last-update').html('Updated: ' + secs + ' seconds ago'); -} - -function start_chat() { - //$('#logout').html('Logout ' + call_sign); - load_messages(); - load_users(); - monitor_last_update(); - setInterval(function() { - load_messages() - }, 15000); - setInterval(function() { - load_users() - }, 15000); - setInterval(function() { - monitor_last_update() - }, 2500); -} - -function meshchat_init() { - $('#message').val(''); - meshchat_id = Cookies.get('meshchat_id'); - if (meshchat_id == undefined) { - Cookies.set('meshchat_id', make_id()); - meshchat_id = Cookies.get('meshchat_id'); - } - //console.log(meshchat_id); - $('#submit-message').on('click', function(e) { - e.preventDefault(); - if ($('#message').val().length == 0) return; - - ohSnapX(); - - $(this).prop("disabled", true); - $('#message').prop("disabled", true); - $(this).html('
'); - - var channel = $('#send-channel').val(); - - if ($('#new-channel').val() != '') { - channel = $('#new-channel').val(); - $('#send-channel').val('Everything'); - } - - $.ajax({ - url: '/cgi-bin/meshchat', - type: "POST", - tryCount : 0, - retryLimit : 3, - cache: false, - timeout: 5000, - data: - { - action: 'send_message', - message: $('#message').val(), - call_sign: call_sign, - epoch: epoch(), - channel: channel - }, - dataType: "json", - context: this, - success: function(data, textStatus, jqXHR) - { - if (data.status == 500) { - ohSnap('Error sending message: ' + data.response, 'red', {time: '30000'}); - } else { - $('#message').val(''); - ohSnap('Message sent', 'green'); - load_messages(); - channel_filter = channel; - $('#new-channel').val(''); - $('#new-channel').hide(); - $('#send-channel').show(); - } - }, - error: function(jqXHR, textStatus, errorThrown) - { - if (textStatus == 'timeout') { - this.tryCount++; - if (this.tryCount <= this.retryLimit) { - //try again - $.ajax(this); - return; - } - ohSnap('Error sending message: ' + textStatus, 'red', {time: '30000'}); - } - }, - complete: function(jqXHR, textStatus) { - $(this).prop("disabled", false); - $('#message').prop("disabled", false); - $(this).html('Send'); - } - }); - }); - - $('#submit-call-sign').on('click', function(e) { - e.preventDefault(); - if ($('#call-sign').val().length == 0) return; - call_sign = $('#call-sign').val().toUpperCase(); - Cookies.set('meshchat_call_sign', call_sign); - $('#call-sign-container').addClass('hidden'); - $('#chat-container').removeClass('hidden'); - $('#callsign').html('Call Sign: ' + Cookies.get('meshchat_call_sign')); - start_chat(); - }); - - $('#channels').on('change', function() { - channel_filter = this.value; - process_messages(); - }); - - $('#search').keyup(function() { - //console.log(this.value); - search_filter = this.value; - process_messages(); - }); - - $('#message-expand').on('click', function(e) { - $('#message-panel').toggleClass('message-panel-collapse'); - $('#message-panel-body').toggleClass('message-panel-body-collapse'); - $('#users-panel').toggleClass('users-panel-collapse'); - $('#users-panel-body').toggleClass('users-panel-body-collapse'); - }); - - $('#send-channel').on('change', function() { - if (this.value == "Add New Channel") { - $('#new-channel').show(); - $(this).hide(); - } - }); - - $('#message').keydown(function (e) { - if ((event.keyCode == 10 || event.keyCode == 13) && event.ctrlKey) { - $("#submit-message").trigger( "click" ); - } - }); - - var cookie_call_sign = Cookies.get('meshchat_call_sign'); - if (cookie_call_sign == undefined) { - $('#call-sign-container').removeClass('hidden'); - } else { - $('#call-sign-container').addClass('hidden'); - $('#chat-container').removeClass('hidden'); - call_sign = cookie_call_sign; - start_chat(); - } -} - -function load_messages() { - if (messages_updating == true) return; - - messages_updating = true; - - $.ajax({ - url: '/cgi-bin/meshchat?action=messages_version_ui&call_sign=' + call_sign + '&id=' + meshchat_id + '&epoch=' + epoch(), - type: "GET", - dataType: "json", - context: this, - cache: false, - success: function(data, textStatus, jqXHR) - { - if (data == null) { - messages_updating = false; - return; - } - - if (data.messages_version != message_db_version) { - pending_message_db_version = data.messages_version; - fetch_messages(); - } else { - messages_updating = false; - last_messages_update = epoch(); - } - }, - error: function(jqXHR, textStatus, errorThrown) - { - messages_updating = false; - }, - complete: function(jqXHR, textStatus) { - - } - }); - -} - -function fetch_messages() { - $.ajax({ - url: '/cgi-bin/meshchat?action=messages&call_sign=' + call_sign + '&id=' + meshchat_id + '&epoch=' + epoch(), - type: "GET", - dataType: "json", - context: this, - cache: false, - success: function(data, textStatus, jqXHR) - { - if (data == null) return; - - messages = data; - - process_messages(); - last_messages_update = epoch(); - message_db_version = pending_message_db_version; - }, - complete: function(jqXHR, textStatus) { - //console.log( "messages complete" ); - messages_updating = false; - } - }); -} - -function process_messages() { - var html = ''; - - var cur_send_channel = $("#send-channel").val(); - - $('#send-channel') - .find('option') - .remove() - .end(); - - $('#channels') - .find('option') - .remove() - .end(); - - var channels = {}; - - var total = 0; - - var search = search_filter.toLowerCase(); - - for (var i = 0; i < messages.length; i++) { - var row = ''; - var date = new Date(0); - date.setUTCSeconds(messages[i].epoch); - var message = messages[i].message; - message = message.replace(/(\r\n|\n|\r)/g, "
"); - - var id = parseInt(messages[i].id, 16); - total += id; - - var formated_date = format_date(date); - - if (search != '') { - //console.log(search); - //console.log(message.toLowerCase()); - if (message.toLowerCase().search(search) == -1 && - messages[i].call_sign.toLowerCase().search(search) == -1 && - messages[i].node.toLowerCase().search(search) == -1 && - formated_date.toLowerCase().search(search) == -1) { - continue; - } - } - - if (messages[i].channel == null) { - messages[i].channel = ""; - } - - row += ''; - row += '' + formated_date + ''; - row += '' + message + ''; - row += '' + messages[i].call_sign + ''; - row += '' + messages[i].channel + ''; - if (messages[i].platform == 'node') { - row += '' + messages[i].node + ''; - } else { - row += '' + messages[i].node + ''; - } - row += ''; - - if (messages[i].channel != "" && !channels.hasOwnProperty(messages[i].channel)) { - channels[messages[i].channel] = 1; - } - - if (channel_filter != '') { - if (channel_filter == messages[i].channel) html += row; - } else { - html += row; - } - } - - if (messages_version != 0) { - if (total != messages_version) { - alert.play(); - } - } - - messages_version = total; - - $('#message-table').html(html); - - $('#send-channel') - .append($("") - .attr("value", "") - .text("Everything")); - - $('#send-channel') - .append($("") - .attr("value", "Add New Channel") - .text("Add New Channel")); - - $('#channels') - .append($("") - .attr("value", "") - .text("Everything")); - - for (var property in channels) { - if (channels.hasOwnProperty(property)) { - $('#send-channel') - .append($("") - .attr("value", property) - .text(property)); - - $('#channels') - .append($("") - .attr("value", property) - .text(property)); - } - } - - $("#channels").val(channel_filter); - $("#send-channel").val(cur_send_channel); -} - -function load_users() { - if (users_updating == true) return; - - users_updating = true; - - $.ajax({ - url: '/cgi-bin/meshchat?action=users&call_sign=' + call_sign + '&id=' + meshchat_id, - type: "GET", - dataType: "json", - context: this, - cache: false, - success: function(data, textStatus, jqXHR) - { - var html = ''; - if (data == null) return; - var count = 0; - for (var i = 0; i < data.length; i++) { - var date = new Date(0); - date.setUTCSeconds(data[i].epoch); - if ((epoch() - data[i].epoch) > 240) continue; - if ((epoch() - data[i].epoch) > 120) { - html += ''; - } else { - html += ''; - } - if (enable_video == 0) { - html += '' + data[i].call_sign + ''; - } else { - html += '' + data[i].call_sign + ''; - } - if (data[i].platform == 'node') { - html += '' + data[i].node + ''; - } else { - html += '' + data[i].node + ''; - } - html += '' + format_date(date) + ''; - html += ''; - - count++; - } - $('#users-table').html(html); - $('#users-count').html(count); - }, - complete: function(jqXHR, textStatus) { - //console.log( "users complete" ); - users_updating = false; - } - }); -} diff --git a/src/data/usr/local/bin/meshchatsync b/support/meshchatsync similarity index 100% rename from src/data/usr/local/bin/meshchatsync rename to support/meshchatsync diff --git a/src/data/etc/init.d/meshchatsync b/support/meshchatsync-init.d similarity index 100% rename from src/data/etc/init.d/meshchatsync rename to support/meshchatsync-init.d diff --git a/src/data/www/meshchat/alert.mp3 b/www/alert.mp3 similarity index 100% rename from src/data/www/meshchat/alert.mp3 rename to www/alert.mp3 diff --git a/www/chat.js b/www/chat.js new file mode 100644 index 0000000..31a503f --- /dev/null +++ b/www/chat.js @@ -0,0 +1,297 @@ +var meshchat_id; +var last_messages_update = epoch(); +var call_sign = 'NOCALL'; +var enable_video = 0; + +var messages = new Messages(); +let alert = new Audio('alert.mp3'); + +let config = {}; +let context = { + config_loaded: false, + debug: true, // let startup funcs show debug +} + +$(function() { + meshchat_init(); +}); + +function monitor_last_update() { + var secs = epoch() - last_messages_update; + $('#last-update').html('Updated: ' + secs + ' seconds ago'); +} + +function update_messages(reason=Messages.MSG_UPDATE) { + if (reason != Messages.MSG_UPDATE) return; + let caller = (new Error()).stack.split("\n")[3].split("/")[0]; + console.debug(caller + "->update_messages(reason=MSG_UPDATE)"); + + // update the message table + let html = messages.render($('#channels').val(), $('#search').val()); + if (html) $('#message-table').html(html); + last_messages_update = epoch(); +} + +function new_messages(reason) { + if (reason != Messages.NEW_MSG) return; + let caller = (new Error()).stack.split("\n")[3].split("/")[0]; + console.debug(caller + "->new_messages(reason=NEW_MSG)"); + alert.play(); +} + +function update_channels(reason) { + if (reason != Messages.CHAN_UPDATE) return; + let caller = (new Error()).stack.split("\n")[3].split("/")[0]; + console.debug(caller + "->update_channels(reason=CHAN_UPDATE)"); + + let msg_refresh = false; + let channels = messages.channels().sort(); + let channel_filter = $('#channels').val(); + let cur_send_channel = $('#send-channel').val(); + // null signals a new channel was just created + if (cur_send_channel == null) { + channel_filter = messages.current_channel(); + cur_send_channel = messages.current_channel(); + msg_refresh = true; + } + + // clear channel selection boxes + $('#send-channel').find('option').remove().end(); + $('#channels').find('option').remove().end(); + + function add_option(select, title, value) { + select.append(""); + } + + // Add static channels to channel selection boxes + add_option($('#send-channel'), "Everything", ""); + add_option($('#send-channel'), "Add New Channel", "Add New Channel"); + add_option($('#channels'), "Everything", ""); + + for (var chan of channels) { + if (chan != "") { + add_option($('#send-channel'), chan, chan); + add_option($('#channels'), chan, chan); + } + } + + $("#channels").val(channel_filter); + $("#send-channel").val(cur_send_channel); + if (msg_refresh) update_messages(); +} + +function start_chat() { + debug("start_chat()"); + + // wait until the configuration is fully loaded + load_config().then(function(data) { + config = data; + document.title = 'Mesh Chat v' + data.version; + $('#version').html('Mesh Chat v' + data.version + ''); + $('#node').html('Node: ' + data.node); + $('#zone').html('Zone: ' + data.zone); + $('#callsign').html('Call Sign: ' + Cookies.get('meshchat_call_sign')); + $('#copyright').html('Mesh Chat v' + data.version + ' Copyright © ' + new Date().getFullYear() + ' Trevor Paskett - K7FPV (Lua by KN6PLV)'); + + if ("default_channel" in data) { + default_channel = data.default_channel; + $('#send-channel').val(data.default_channel); + $('#channels').val(data.default_channel); + messages.set_channel(data.default_channel); + update_messages(); + } + + if ("debug" in data) { + context.debug = data.debug == 1 ? true : false; + } + + // signal that the config has finished loading + context.config_loaded = true; + }) + + //$('#logout').html('Logout ' + call_sign); + messages.subscribe(update_messages); + messages.subscribe(new_messages); + messages.subscribe(update_channels); + messages.check(); + load_users(); + monitor_last_update(); + + // start event loops to update MeshChat client + setInterval(() => { messages.check() }, 15000); + setInterval(() => { load_users() }, 15000); + setInterval(() => { monitor_last_update() }, 2500); +} + +function meshchat_init() { + debug("meshchat_init()"); + + $('#message').val(''); + meshchat_id = Cookies.get('meshchat_id'); + if (meshchat_id == undefined) { + // TODO set default expiration of cookie + Cookies.set('meshchat_id', make_id()); + meshchat_id = Cookies.get('meshchat_id'); + } + + $('#submit-message').on('click', function(e) { + e.preventDefault(); + if ($('#message').val().length == 0) return; + + ohSnapX(); + + // disable message sending box + $(this).prop("disabled", true); + $('#message').prop("disabled", true); + $(this).html('
'); + + let channel = $('#send-channel').val(); + + if ($('#new-channel').val() != '') { + channel = $('#new-channel').val(); + $('#send-channel').val('Everything'); + } + + messages.send($('#message').val(), channel, call_sign).then( + // sent + (sent) => { + $('#message').val(''); + ohSnap('Message sent', 'green'); + update_messages(Messages.NEW_MSG); + + // clear out new channel box in case it was used and + // reset to normal selection box + $('#new-channel').val(''); + $('#new-channel').hide(); + $('#send-channel').show(); + }, + // error + (err_msg) => { + ohSnap(err_msg, 'red', {time: '30000'}); + } + ).finally(() => { + // change the channel selector to the channel the message was + // just sent to + $('#channels').val(channel); + messages.set_channel(channel); + update_messages(); + + // re-enable message sending box + $(this).prop("disabled", false); + $('#message').prop("disabled", false); + $(this).html('Send'); + }); + }); + + $('#submit-call-sign').on('click', function(e) { + e.preventDefault(); + if ($('#call-sign').val().length == 0) return; + call_sign = $('#call-sign').val().toUpperCase(); + // TODO set default expiration of cookie + Cookies.set('meshchat_call_sign', call_sign); + $('#call-sign-container').addClass('hidden'); + $('#chat-container').removeClass('hidden'); + $('#callsign').html('Call Sign: ' + call_sign); + start_chat(); + }); + + $('#channels').on('change', function() { + $('#send-channel').val(this.value); + messages.set_channel(this.value); + update_messages(); + }); + + $('#search').keyup(function() { + //console.log(this.value); + update_messages(); + }); + + $('#message-expand').on('click', function(e) { + $('#message-panel').toggleClass('message-panel-collapse'); + $('#message-panel-body').toggleClass('message-panel-body-collapse'); + $('#users-panel').toggleClass('users-panel-collapse'); + $('#users-panel-body').toggleClass('users-panel-body-collapse'); + }); + + // allow user to enter new channel + $('#send-channel').on('change', function() { + if (this.value == "Add New Channel") { + $('#new-channel').show().focus(); + $(this).hide(); + } + }); + + // process a CTRL to send a message + $('#message').keydown(function (e) { + if ((e.keyCode == 10 || e.keyCode == 13) && e.ctrlKey) { + $("#submit-message").trigger( "click" ); + } + }); + + // login with a cookie + var cookie_call_sign = Cookies.get('meshchat_call_sign'); + if (cookie_call_sign == undefined) { + $('#call-sign-container').removeClass('hidden'); + } else { + $('#call-sign-container').addClass('hidden'); + $('#chat-container').removeClass('hidden'); + call_sign = cookie_call_sign; + start_chat(); + } +} + +let users_updating = false; +function load_users() { + debug("load_users()"); + + if (users_updating == true) return; + console.debug("load_users()"); + + // lock to prevent simultaneous updates + users_updating = true; + + $.getJSON('/cgi-bin/meshchat?action=users&call_sign=' + call_sign + '&id=' + meshchat_id, + (data) => { + if (data == null || data == 0) return; + + let html = ''; + let count = 0; + + for (var entry of data) { + var date = new Date(0); + date.setUTCSeconds(entry.epoch); + + // user heartbeat timeout > 4 mins + if ((epoch() - entry.epoch) > 240) continue; + + // user heartbeat > 2 mins, expiring + if ((epoch() - entry.epoch) > 120) { + html += ''; + } else { + html += ''; + } + + if (enable_video == 0) { + html += '' + entry.call_sign + ''; + } else { + html += '' + entry.call_sign + ''; + } + + if (entry.platform == 'node') { + html += '' + entry.node + ''; + } else { + html += '' + entry.node + ''; + } + + html += '' + format_date(date) + ''; + html += ''; + + count++; + } + $('#users-table').html(html); + $('#users-count').html(count); + }).always(() => { + // allow updates again + users_updating = false; + }); +} diff --git a/src/data/www/meshchat/files.html b/www/files.html similarity index 100% rename from src/data/www/meshchat/files.html rename to www/files.html diff --git a/src/data/www/meshchat/files.js b/www/files.js similarity index 100% rename from src/data/www/meshchat/files.js rename to www/files.js diff --git a/src/data/www/meshchat/index.html b/www/index.html similarity index 91% rename from src/data/www/meshchat/index.html rename to www/index.html index 44d7c04..1cae316 100644 --- a/src/data/www/meshchat/index.html +++ b/www/index.html @@ -5,7 +5,7 @@ Mesh Chat - + @@ -78,13 +78,13 @@ Mesh Chat - +
-
+
-
+
@@ -125,15 +125,15 @@ +
- +
@@ -154,13 +154,13 @@ - +
- +
@@ -173,11 +173,11 @@     - Channel: + Channel:
@@ -209,9 +209,10 @@ - + --> - + + Loading messages....
@@ -235,9 +236,13 @@ + + + diff --git a/src/data/www/meshchat/jquery-2.2.0.min.js b/www/jquery-2.2.0.min.js similarity index 100% rename from src/data/www/meshchat/jquery-2.2.0.min.js rename to www/jquery-2.2.0.min.js diff --git a/src/data/www/meshchat/js.cookie.js b/www/js.cookie.js similarity index 100% rename from src/data/www/meshchat/js.cookie.js rename to www/js.cookie.js diff --git a/www/md5.js b/www/md5.js new file mode 100644 index 0000000..60cae36 --- /dev/null +++ b/www/md5.js @@ -0,0 +1,192 @@ +/* + MD5 code copyright (c) by Joseph Myers + http://www.myersdaily.org/joseph/javascript/md5-text.html +*/ +function md5cycle(x, k) { + var a = x[0], b = x[1], c = x[2], d = x[3]; + + a = ff(a, b, c, d, k[0], 7, -680876936); + d = ff(d, a, b, c, k[1], 12, -389564586); + c = ff(c, d, a, b, k[2], 17, 606105819); + b = ff(b, c, d, a, k[3], 22, -1044525330); + a = ff(a, b, c, d, k[4], 7, -176418897); + d = ff(d, a, b, c, k[5], 12, 1200080426); + c = ff(c, d, a, b, k[6], 17, -1473231341); + b = ff(b, c, d, a, k[7], 22, -45705983); + a = ff(a, b, c, d, k[8], 7, 1770035416); + d = ff(d, a, b, c, k[9], 12, -1958414417); + c = ff(c, d, a, b, k[10], 17, -42063); + b = ff(b, c, d, a, k[11], 22, -1990404162); + a = ff(a, b, c, d, k[12], 7, 1804603682); + d = ff(d, a, b, c, k[13], 12, -40341101); + c = ff(c, d, a, b, k[14], 17, -1502002290); + b = ff(b, c, d, a, k[15], 22, 1236535329); + + a = gg(a, b, c, d, k[1], 5, -165796510); + d = gg(d, a, b, c, k[6], 9, -1069501632); + c = gg(c, d, a, b, k[11], 14, 643717713); + b = gg(b, c, d, a, k[0], 20, -373897302); + a = gg(a, b, c, d, k[5], 5, -701558691); + d = gg(d, a, b, c, k[10], 9, 38016083); + c = gg(c, d, a, b, k[15], 14, -660478335); + b = gg(b, c, d, a, k[4], 20, -405537848); + a = gg(a, b, c, d, k[9], 5, 568446438); + d = gg(d, a, b, c, k[14], 9, -1019803690); + c = gg(c, d, a, b, k[3], 14, -187363961); + b = gg(b, c, d, a, k[8], 20, 1163531501); + a = gg(a, b, c, d, k[13], 5, -1444681467); + d = gg(d, a, b, c, k[2], 9, -51403784); + c = gg(c, d, a, b, k[7], 14, 1735328473); + b = gg(b, c, d, a, k[12], 20, -1926607734); + + a = hh(a, b, c, d, k[5], 4, -378558); + d = hh(d, a, b, c, k[8], 11, -2022574463); + c = hh(c, d, a, b, k[11], 16, 1839030562); + b = hh(b, c, d, a, k[14], 23, -35309556); + a = hh(a, b, c, d, k[1], 4, -1530992060); + d = hh(d, a, b, c, k[4], 11, 1272893353); + c = hh(c, d, a, b, k[7], 16, -155497632); + b = hh(b, c, d, a, k[10], 23, -1094730640); + a = hh(a, b, c, d, k[13], 4, 681279174); + d = hh(d, a, b, c, k[0], 11, -358537222); + c = hh(c, d, a, b, k[3], 16, -722521979); + b = hh(b, c, d, a, k[6], 23, 76029189); + a = hh(a, b, c, d, k[9], 4, -640364487); + d = hh(d, a, b, c, k[12], 11, -421815835); + c = hh(c, d, a, b, k[15], 16, 530742520); + b = hh(b, c, d, a, k[2], 23, -995338651); + + a = ii(a, b, c, d, k[0], 6, -198630844); + d = ii(d, a, b, c, k[7], 10, 1126891415); + c = ii(c, d, a, b, k[14], 15, -1416354905); + b = ii(b, c, d, a, k[5], 21, -57434055); + a = ii(a, b, c, d, k[12], 6, 1700485571); + d = ii(d, a, b, c, k[3], 10, -1894986606); + c = ii(c, d, a, b, k[10], 15, -1051523); + b = ii(b, c, d, a, k[1], 21, -2054922799); + a = ii(a, b, c, d, k[8], 6, 1873313359); + d = ii(d, a, b, c, k[15], 10, -30611744); + c = ii(c, d, a, b, k[6], 15, -1560198380); + b = ii(b, c, d, a, k[13], 21, 1309151649); + a = ii(a, b, c, d, k[4], 6, -145523070); + d = ii(d, a, b, c, k[11], 10, -1120210379); + c = ii(c, d, a, b, k[2], 15, 718787259); + b = ii(b, c, d, a, k[9], 21, -343485551); + + x[0] = add32(a, x[0]); + x[1] = add32(b, x[1]); + x[2] = add32(c, x[2]); + x[3] = add32(d, x[3]); +} + +function cmn(q, a, b, x, s, t) { + a = add32(add32(a, q), add32(x, t)); + return add32((a << s) | (a >>> (32 - s)), b); +} + +function ff(a, b, c, d, x, s, t) { + return cmn((b & c) | ((~b) & d), a, b, x, s, t); +} + +function gg(a, b, c, d, x, s, t) { + return cmn((b & d) | (c & (~d)), a, b, x, s, t); +} + +function hh(a, b, c, d, x, s, t) { + return cmn(b ^ c ^ d, a, b, x, s, t); +} + +function ii(a, b, c, d, x, s, t) { + return cmn(c ^ (b | (~d)), a, b, x, s, t); +} + +function md51(s) { + txt = ''; + var n = s.length, + state = [1732584193, -271733879, -1732584194, 271733878], i; + + for (i=64; i<=s.length; i+=64) { + md5cycle(state, md5blk(s.substring(i-64, i))); + } + + s = s.substring(i-64); + var tail = [0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0]; + for (i=0; i>2] |= s.charCodeAt(i) << ((i%4) << 3); + tail[i>>2] |= 0x80 << ((i%4) << 3); + + if (i > 55) { + md5cycle(state, tail); + for (i=0; i<16; i++) tail[i] = 0; + } + + tail[14] = n*8; + md5cycle(state, tail); + return state; +} + +/* there needs to be support for Unicode here, + * unless we pretend that we can redefine the MD-5 + * algorithm for multi-byte characters (perhaps + * by adding every four 16-bit characters and + * shortening the sum to 32 bits). Otherwise + * I suggest performing MD-5 as if every character + * was two bytes--e.g., 0040 0025 = @%--but then + * how will an ordinary MD-5 sum be matched? + * There is no way to standardize text to something + * like UTF-8 before transformation; speed cost is + * utterly prohibitive. The JavaScript standard + * itself needs to look at this: it should start + * providing access to strings as preformed UTF-8 + * 8-bit unsigned value arrays. + */ +function md5blk(s) { /* I figured global was faster. */ + var md5blks = [], i; /* Andy King said do it this way. */ + + for (i=0; i<64; i+=4) { + md5blks[i>>2] = s.charCodeAt(i) + + (s.charCodeAt(i+1) << 8) + + (s.charCodeAt(i+2) << 16) + + (s.charCodeAt(i+3) << 24); + } + return md5blks; +} + +var hex_chr = '0123456789abcdef'.split(''); + +function rhex(n) { + var s='', j=0; + + for(; j<4; j++) + s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + + hex_chr[(n >> (j * 8)) & 0x0F]; + return s; +} + +function hex(x) { + for (var i=0; i> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } +} diff --git a/www/messages.js b/www/messages.js new file mode 100644 index 0000000..f1c1038 --- /dev/null +++ b/www/messages.js @@ -0,0 +1,303 @@ + + +// Messages is a singleton that keeps a copy of all messages +class Messages { + + static NEW_MSG = 1; + static CHAN_UPDATE = 2; + static MSG_UPDATE = 3; + + constructor() { + if (! this.__instance) { + this.messages = new Map(); + this.message_order = new Array(); + this.delete_list = new Array(); // future enhancement + this.db_version = 0; + this.last_update_time = 0; + this._updating = false; + this._message_checksum = null; // only messages in channel + + this.__current_channel = ""; + this.__channels = new Array(); + this.__observers = new Array(); + + this.__instance = this; + } + return this.__instance; + } + + // return reference to singleton, creating if necessary + getInstance() { + if (! this.__instance) { + this.__instance = new Messages(); + } + return this.__instance; + } + + /* check() retrieves the current message database version from the + MeshChat server and compares it with the last known version. + If the database version is different (i.e. database has new messages), + then an update cycle is kicked off by calling fetch() */ + check() { + console.debug("Messages.check()"); + + var pending_db_version = 0; + + // currently updating, ignore this check + if (this._updating == true) { + console.debug("Message.check() skipped due to messages being updated."); + return; + } + + // lock out all other updates + this._updating = true; + + $.getJSON('/cgi-bin/meshchat?action=messages_version_ui&call_sign=' + call_sign + '&id=' + meshchat_id + '&epoch=' + epoch(), + (data) => { + if (data == null || data == 0) { + this._updating = false; + } else if ('messages_version' in data && this.db_version != data.messages_version) { + this.fetch(data.messages_version); + } else { + this._updating = false; + } + }).fail((error) => { + // TODO error message on UI describing failure + this._updating = false; + }); + } + + /* fetch() is used to retrieve the messages from the message database. + It is told the new database version with the pending_version param. + All messages are then stored in the local message db (this.messages) + and update() is called to update all the internal counters */ + fetch(pending_version) { + console.debug("Messages.fetch(pending_version = " + pending_version + ")"); + + $.getJSON('/cgi-bin/meshchat?action=messages&call_sign=' + call_sign + '&id=' + meshchat_id + '&epoch=' + epoch(), + (data) => { + if (data == null || data == 0) empty(); + + // integrate new messages into the message DB + data.forEach((entry) => { this.messages.set(entry.id, entry) }); + + this.update(); + this.last_update_time = epoch(); + this.db_version = pending_version; + this._updating = false; + this.notify(Messages.MSG_UPDATE); + this.notify(Messages.CHAN_UPDATE); + }).fail((error) => { + // TODO error message on UI describing failure + this._updating = false; + }); + } + + /* update the message DB with counts, channels, etc. + If msg_ids is not specified, then process all messages in the + DB */ + update(msg_ids=null) { + console.debug("Messages.update(msg_ids=" + JSON.stringify(msg_ids) + " )"); + + if (msg_ids === null) { + msg_ids = Array.from(this.messages.keys()); + } + + for (var id of msg_ids.values()) { + var message = this.messages.get(id); + + // if there is not a message don't try to process it. + if (message === undefined) { + // throw error message + continue; + } + + // null channel names is the Everything channel (empty string) + if (message.channel === null) { + message.channel = ""; + } + + // update list of available channels + if (! this.__channels.includes(message.channel)) { + this.__channels.push(message.channel); + } + + // TODO not sure this is actually needed, get should be returning a reference + this.messages.set(id, message); + } + + // sort the messages by time (descending) + this.message_order = Array.from(this.messages.keys()).sort( + (a,b) => { + let a_msg = this.messages.get(a); + let b_msg = this.messages.get(b); + return a_msg.epoch > b_msg.epoch ? -1 : 1; + }); + } + + set_channel(chan) { + console.debug("Messages.set_channel(chan=" + chan + ")"); + this.__current_channel = chan; + this._message_checksum = null; + } + + current_channel() { + return this.__current_channel; + } + + // return a list of channels available across all messages + channels() { + return Array.from(this.__channels.values()); + } + + send(message, channel, call_sign) { + console.debug("Messages.send(message='" + message +"', channel=" + channel + ", call_sign=" + call_sign + ")"); + let params = { + action: 'send_message', + message: message, + call_sign: call_sign, + epoch: epoch(), + id: this._create_id(), + channel: channel + }; + + // { timeout: 5000, retryLimit: 3, dataType: "json", data: params} + return new Promise((sent, error) => { + $.post('/cgi-bin/meshchat', params, + (data) => { + if (data.status == 500) { + error('Error sending message: ' + data.response); + } else { + // add the message to the in memory message DB + this.messages.set(params['id'], { + id: params['id'], + message: message, + call_sign: call_sign, + epoch: params['epoch'], + channel: channel, + node: node_name(), + platform: platform(), + }); + + // Add the channel to the list + if (! channel in this.channels()) { + this.__channels.push(channel); + this.set_channel(channel); + this.notify(Messages.CHAN_UPDATE); + } + + // update internal message checksum with locally + // created message ID so not to trigger alert sound + this._message_checksum += parseInt(params['id'], 16); + this.update(); + this.notify(Messages.MSG_UPDATE); + sent(); + } + }).fail((error) => { + if (error == 'timeout') { + this.tryCount++; + if (this.tryCount <= this.retryLimit) { + //try again + $.ajax(this); + return; + } + error(error); + } + }); + }) + + } + + // return a rendered version of a block of messages + render(channel, search_filter) { + console.debug("Messages.render(channel=" + channel + ", search_filter=" + search_filter + ")"); + let html = ''; + let search = search_filter.toLowerCase(); + let message_checksum = 0; + + for (var id of this.message_order) { + var message = this.messages.get(id); + + // calculate the date for the current message + let date = new Date(0); + date.setUTCSeconds(message.epoch); + message.date = date; + + if (search != '') { + //console.log(search); + //console.log(message.toLowerCase()); + if (message.message.toLowerCase().search(search) == -1 && + message.call_sign.toLowerCase().search(search) == -1 && + message.node.toLowerCase().search(search) == -1 && + format_date(date).toLowerCase().search(search) == -1) { + continue; + } + } + + if (channel == message.channel || this.__current_channel == '') { + html += this.render_row(message); + + // add this message to the checksum + message_checksum += parseInt(message.id, 16); + } + } + + // provide a message if no messages were found + if (html == "") { + html = "No messages found"; + } + + // this._message_checksum == null is the first rendering of the + // message table. No need to sound an alert. + if (this._message_checksum != null && message_checksum != this._message_checksum) { + this.notify(Messages.NEW_MSG); + } + this._message_checksum = message_checksum; + + return html; + } + + render_row(msg_data) { + let message = msg_data.message.replace(/(\r\n|\n|\r)/g, "
"); + + let row = ''; + if (false) { + row += '' + msg_data.id + ''; + } + row += '' + format_date(msg_data.date) + ''; + row += '' + message + ''; + row += '' + msg_data.call_sign + ''; + row += '' + msg_data.channel + ''; + if (msg_data.platform == 'node') { + row += '' + msg_data.node + ''; + } else { + row += '' + msg_data.node + ''; + } + row += ''; + + return row; + } + + // generate unique message IDs + _create_id() { + let seed = epoch().toString() + Math.floor(Math.random() * 99999); + let hash = md5(seed); + return hash.substring(0,8); + } + + // Observer functions + subscribe(func) { + console.debug("Messages.subscribe(func=" + func.name + ")"); + this.__observers.push(func); + } + + unsubscribe(func) { + console.debug("Messages.unsubscribe(func=" + func + ")"); + this.__observers = this.__observers.filter((observer) => observer !== func); + } + + notify(reason) { + console.debug("Messages.notify(reason=" + reason + ")"); + this.__observers.forEach((observer) => observer(reason)); + } +} diff --git a/src/data/www/meshchat/normalize.css b/www/normalize.css similarity index 100% rename from src/data/www/meshchat/normalize.css rename to www/normalize.css diff --git a/src/data/www/meshchat/numeral.min.js b/www/numeral.min.js similarity index 100% rename from src/data/www/meshchat/numeral.min.js rename to www/numeral.min.js diff --git a/src/data/www/meshchat/ohsnap.js b/www/ohsnap.js similarity index 100% rename from src/data/www/meshchat/ohsnap.js rename to www/ohsnap.js diff --git a/src/data/www/meshchat/shared.js b/www/shared.js similarity index 82% rename from src/data/www/meshchat/shared.js rename to www/shared.js index 2f93179..ad49b9d 100644 --- a/src/data/www/meshchat/shared.js +++ b/www/shared.js @@ -1,11 +1,15 @@ +var config; + $(function() { $('#logout').on('click', function(e){ - e.preventDefault(); - Cookies.remove('meshchat_call_sign'); - window.location = '/meshchat'; + e.preventDefault(); + Cookies.remove('meshchat_call_sign'); + window.location = '/meshchat'; }); $.getJSON('/cgi-bin/meshchat?action=config', function(data) { + config = data; + document.title = 'Mesh Chat v' + data.version; $('#version').html('Mesh Chat v' + data.version + ''); $('#node').html('Node: ' + data.node); @@ -15,13 +19,21 @@ $(function() { }); }); +function node_name() { + return config.node; +} + +function platform() { + return config.platform || 'node'; // TODO temp patch until config API is updated +} + function epoch() { return Math.floor(new Date() / 1000); } function format_date(date) { var string; - + var year = String(date.getFullYear()); string = (date.getMonth()+1) + '/' + date.getDate() + '/' + year.slice(-2); @@ -58,3 +70,7 @@ function aredn_domain(host) { host = host.split(":") return host[0] + ".local.mesh" + (host[1] ? ":" + host[1] : ""); } + +function debug(msg) { + context.debug && console.debug(msg); +} diff --git a/src/data/www/meshchat/skeleton.css b/www/skeleton.css similarity index 100% rename from src/data/www/meshchat/skeleton.css rename to www/skeleton.css diff --git a/src/data/www/meshchat/status.html b/www/status.html similarity index 100% rename from src/data/www/meshchat/status.html rename to www/status.html diff --git a/src/data/www/meshchat/status.js b/www/status.js similarity index 100% rename from src/data/www/meshchat/status.js rename to www/status.js diff --git a/src/data/www/meshchat/style.css b/www/style.css similarity index 100% rename from src/data/www/meshchat/style.css rename to www/style.css