Compare commits

...

36 commits
main ... future

Author SHA1 Message Date
Mark Joshwel a6ee760eff bridges(wa): bump to v2.2024.34
Some checks failed
continuous deployment: WhatsApp Bridge / build (push) Has been cancelled
continuous integration: WhatsApp Bridge / check (push) Has been cancelled
2024-08-25 03:51:53 +08:00
Mark Joshwel b5bab89c49 bridges: w29 bump
Some checks failed
continuous deployment: Telegram Bridge / build (push) Has been cancelled
continuous deployment: WhatsApp Bridge / build (push) Has been cancelled
continuous integration: Telegram Bridge / check (push) Has been cancelled
continuous integration: WhatsApp Bridge / check (push) Has been cancelled
2024-07-17 09:53:21 +08:00
dependabot[bot] 296fa739f0 build(deps-dev): bump ruff in /src/spow-telegram-bridge
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.9 to 0.5.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.9...0.5.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-16 19:19:18 +08:00
dependabot[bot] 21c6b11f65 build(deps-dev): bump mypy in /src/spow-telegram-bridge
Bumps [mypy](https://github.com/python/mypy) from 1.10.0 to 1.10.1.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.10.0...v1.10.1)

---
updated-dependencies:
- dependency-name: mypy
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-16 19:16:12 +08:00
Mark Joshwel ffd81b8856 whatsapp: 2.2024.26
Some checks failed
continuous deployment: WhatsApp Bridge / build (push) Has been cancelled
continuous integration: WhatsApp Bridge / check (push) Has been cancelled
2024-06-25 16:11:52 +08:00
Mark Joshwel 60fd8e8b8a
cd(docs): push to cf pages' main branch 2024-06-19 03:25:34 +08:00
Mark Joshwel 30815f08b8 ci/cd: fix permission issue 2024-06-19 01:46:17 +08:00
Mark Joshwel 3f6327a134 ci/cd: add build and check workflows
Some checks failed
continuous deployment: surplus Documentation / publish surplus Documentation (push) Has been cancelled
continuous deployment: Telegram Bridge / build (push) Has been cancelled
continuous deployment: WhatsApp Bridge / build (push) Has been cancelled
continuous integration: s+ow / check (push) Has been cancelled
continuous integration: Telegram Bridge / check (push) Has been cancelled
continuous integration: WhatsApp Bridge / check (push) Has been cancelled
2024-06-19 01:32:54 +08:00
Mark Joshwel fe5c47e21a tools(docs-prebuild): output something 2024-06-19 01:32:03 +08:00
Mark Joshwel ad5707408c whatsapp: check comply, make native built default 2024-06-19 01:31:35 +08:00
Mark Joshwel 85f305fb78 telegram: add mypy and isort 2024-06-19 01:30:41 +08:00
Mark Joshwel ea30737ae6 add check.sh scripts 2024-06-19 01:30:04 +08:00
Mark Joshwel c04bcb8831 meta: (flake.)nix pill 2024-06-18 19:00:55 +08:00
Mark Joshwel 59f28ce868 s+: add readme 2024-06-18 19:00:21 +08:00
Mark Joshwel c6eadc6045 docs: add mkdocs site :O 2024-06-18 18:59:55 +08:00
Mark Joshwel 40d318bef7 s+ow: update files 2024-06-18 18:58:39 +08:00
Mark Joshwel 44907e3462 whatsapp: v2.2024.25 2024-06-18 18:57:56 +08:00
Mark Joshwel 93e868845a telegram: v2.2024.25 2024-06-18 18:57:28 +08:00
Mark Joshwel 387d2a222a s+: update root files 2024-06-18 18:55:15 +08:00
Mark Joshwel 822eee5b11 ci/cd: remove workflows for the time being; add dependabot 2024-06-18 18:54:41 +08:00
Mark Joshwel 5737d22237 many: monorepo-ise 2024-06-18 18:53:38 +08:00
Mark Joshwel d64eb987a9 ci: attempt to fix ruff in ci 2024-03-26 19:11:42 +00:00
Mark Joshwel 4a10586824 ci: fix check invocation 2024-03-26 19:04:56 +00:00
Mark Joshwel a9dbc082ae devbox: relock 2024-03-26 19:04:45 +00:00
Mark Joshwel 829eccc4c6 ci: update to checkout current branch 2024-03-26 19:01:19 +00:00
Mark Joshwel c718b6544e devbox: remove shell stuff 2024-03-26 18:57:17 +00:00
Mark Joshwel fb45c2177b meta: add hatch fmt to check script 2024-03-26 18:52:23 +00:00
Mark Joshwel f73cee3684 s+: ruff comply 2024-03-26 18:52:05 +00:00
Mark Joshwel 4a2ea88fca docs: split up docs
these will be improved on a later date
2024-03-26 18:48:13 +00:00
Mark Joshwel d22fee2b0b ci: update workflows to use hatch 2024-03-26 18:46:39 +00:00
Mark Joshwel 56694ffa68 meta: remove test.py 2024-03-26 18:45:15 +00:00
Mark Joshwel 8a59994894 s+: reformat and add --show-user-agent 2024-03-26 18:45:07 +00:00
Mark Joshwel a456a59951 meta: we now use hatch 2024-03-26 18:44:41 +00:00
Mark Joshwel 1ccd66b166 code: pass ruff 2024-03-26 18:08:49 +00:00
Mark Joshwel 4871b9d352 cd: on every push 2024-03-26 09:54:52 +00:00
Mark Joshwel 8e83084f8c chore(ci,poetry,devbox): the great update 2024-03-26 09:52:53 +00:00
81 changed files with 5817 additions and 2455 deletions

View file

@ -1,7 +1,14 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2 version: 2
updates: updates:
- package-ecosystem: "pip" - package-ecosystem: "pip"
directory: "/" directory: "/src/surplus"
schedule: schedule:
interval: "weekly" interval: "weekly"
- package-ecosystem: "pip"
directory: "/src/spow-telegram-bridge"
schedule:
interval: "daily"
- package-ecosystem: gomod
directory: "/src/spow-whatsapp-bridge"
schedule:
interval: "daily"

42
.github/workflows/cd-docs.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: "continuous deployment: surplus Documentation"
on:
workflow_dispatch:
push:
paths:
- "docs/**"
- "mkdocs.yml"
- "src/spow-telegram-bridge/*.py"
- "src/**/*LICENCE*"
- "docs/**/*LICENCE*"
- "docs/**/*LICENSE*"
- "docs/CC0"
- "*LICENCE*"
- "src/surplus-on-wheels/s+ow"
- "src/surplus-on-wheels/install.sh"
- "src/spow-whatsapp-bridge/install.sh"
- "src/spow-telegram-bridge/install.sh"
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- run: nix develop --impure --command hatch run docs:build
- uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: surplus
directory: site
branch: main
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

28
.github/workflows/cd-telegram.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: "continuous deployment: Telegram Bridge"
on:
workflow_dispatch:
push:
paths:
- "src/spow-telegram-bridge/**"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: |
cd src/spow-telegram-bridge
nix develop --command poetry build
- uses: actions/upload-artifact@v4
with:
name: "spow-telegram-bridge"
path: src/spow-whatsapp-bridge/dist
retention-days: 14

28
.github/workflows/cd-whatsapp.yml vendored Normal file
View file

@ -0,0 +1,28 @@
name: "continuous deployment: WhatsApp Bridge"
on:
workflow_dispatch:
push:
paths:
- "src/spow-whatsapp-bridge/**"
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: |
cd src/spow-whatsapp-bridge
nix build .#termux
- uses: actions/upload-artifact@v4
with:
name: "spow-whatsapp-bridge-android"
path: src/spow-whatsapp-bridge/result
retention-days: 14

View file

@ -1,45 +0,0 @@
name: qc
on:
workflow_dispatch:
jobs:
analyse:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: install devbox
uses: jetpack-io/devbox-install-action@v0.7.0
- name: install dependencies
run: devbox run poetry install
- name: build wheel
id: build
run: devbox run poetry build
- name: analyse with mypy
run: devbox run poetry run mypy .
- name: check for black formatting compliance
run: devbox run poetry run black --check .
- name: analyse isort compliance
run: devbox run poetry run isort --check *.py **/*.py
test:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
- name: install devbox
uses: jetpack-io/devbox-install-action@v0.7.0
- name: install dependencies
run: devbox run poetry install
- name: run tests
run: devbox run poetry run python test.py

22
.github/workflows/ci-spow.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: "continuous integration: s+ow"
on:
workflow_dispatch:
push:
paths:
- "src/surplus-on-wheels/**"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: |
cd src/spow-whatsapp-bridge
nix develop --command sh check.sh

22
.github/workflows/ci-telegram.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: "continuous integration: Telegram Bridge"
on:
workflow_dispatch:
push:
paths:
- "src/spow-telegram-bridge/**"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: |
cd src/spow-telegram-bridge
nix develop --command sh check.sh

22
.github/workflows/ci-whatsapp.yml vendored Normal file
View file

@ -0,0 +1,22 @@
name: "continuous integration: WhatsApp Bridge"
on:
workflow_dispatch:
push:
paths:
- "src/spow-whatsapp-bridge/**"
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: cachix/install-nix-action@v27
with:
nix_path: nixpkgs=channel:nixos-unstable
github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: DeterminateSystems/magic-nix-cache-action@main
- run: |
cd src/spow-whatsapp-bridge
nix develop --command sh check.sh

View file

@ -1,72 +0,0 @@
name: automated tagged release with slsa 3 compliance
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: checkout
uses: actions/checkout@v3
with:
ref: main
- name: get branch name
id: get-branch-name
uses: tj-actions/branch-names@v7
- name: install devbox
uses: jetpack-io/devbox-install-action@v0.7.0
- name: install dependencies
run: devbox run poetry install
- name: run releaser.py
run: devbox run python releaser.py
env:
SURPLUS_BUILD_BRANCH: ${{ steps.get-branch-name.outputs.base_ref_branch }}
- name: build project
id: build
run: devbox run poetry build
- name: duplicate non-versioned wheel
run: cp dist/surplus-*.whl dist/surplus-latest-py3-none-any.whl
- name: generate provenance subjects
id: hash
run: |
cd dist
HASHES=$(sha256sum * | base64 -w0)
echo "hashes=$HASHES" >> "$GITHUB_OUTPUT"
- uses: actions/upload-artifact@v3
with:
name: wheels
path: dist/
- name: release
uses: softprops/action-gh-release@v0.1.15
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
dist/*.whl
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.6.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true

View file

@ -1,70 +0,0 @@
name: manual release with slsa 3 compliance
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
hashes: ${{ steps.hash.outputs.hashes }}
steps:
- name: checkout
uses: actions/checkout@v3
with:
ref: main
- name: get branch name
id: get-branch-name
uses: tj-actions/branch-names@v7
- name: install devbox
uses: jetpack-io/devbox-install-action@v0.7.0
- name: install dependencies
run: devbox run poetry install
- name: run releaser.py
run: devbox run python releaser.py
env:
SURPLUS_BUILD_BRANCH: ${{ steps.get-branch-name.outputs.base_ref_branch }}
- name: build project
id: build
run: devbox run poetry build
- name: duplicate non-versioned wheel
run: cp dist/surplus-*.whl dist/surplus-latest-py3-none-any.whl
- name: generate provenance subjects
id: hash
run: |
cd dist
HASHES=$(sha256sum * | base64 -w0)
echo "hashes=$HASHES" >> "$GITHUB_OUTPUT"
- name: release
uses: softprops/action-gh-release@v0.1.15
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
dist/*.whl
- uses: actions/upload-artifact@v3
with:
name: wheels
path: dist/
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v1.6.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: true

11
.gitignore vendored
View file

@ -1,7 +1,18 @@
.idea/
old/*
# cached files # cached files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$py.class
.cache/
# documentation
docs/spow.sh
docs/termux.sh
docs/whatsapp.sh
docs/telegram.sh
/site
# distribution # distribution
.Python .Python

2
CONTRIBUTORS Normal file
View file

@ -0,0 +1,2 @@
Mark Joshwel <mark@joshwel.co>
shamsu07 <shamsuddeenks@gmail.com>

1488
README.md

File diff suppressed because it is too large Load diff

View file

@ -1,16 +0,0 @@
{
"packages": [
"poetry@latest",
"shfmt@latest",
"shellcheck@latest",
"python@latest"
],
"shell": {
"init_hook": [
"poetry install"
]
},
"nixpkgs": {
"commit": "f80ac848e3d6f0c12c52758c0f25c10c97ca3b62"
}
}

View file

@ -1,87 +0,0 @@
{
"lockfile_version": "1",
"packages": {
"poetry@latest": {
"last_modified": "2024-02-10T18:15:24Z",
"plugin_version": "0.0.4",
"resolved": "github:NixOS/nixpkgs/10b813040df67c4039086db0f6eaf65c536886c6#poetry",
"source": "devbox-search",
"version": "1.7.1",
"systems": {
"aarch64-darwin": {
"store_path": "/nix/store/0pf30mblcl4clvagdzybfgfyzjqkjkqi-python3.11-poetry-1.7.1"
},
"aarch64-linux": {
"store_path": "/nix/store/knc1livlnrnaxbnfs9118nq69i78jj39-python3.11-poetry-1.7.1"
},
"x86_64-darwin": {
"store_path": "/nix/store/qf2px4ic22dpra0s6mlmjm5q4vvc6dr7-python3.11-poetry-1.7.1"
},
"x86_64-linux": {
"store_path": "/nix/store/fq4cf1xzwlvbhg98ih8dvsq2hsalhzyp-python3.11-poetry-1.7.1"
}
}
},
"python@latest": {
"last_modified": "2024-02-10T18:15:24Z",
"plugin_version": "0.0.3",
"resolved": "github:NixOS/nixpkgs/10b813040df67c4039086db0f6eaf65c536886c6#python312",
"source": "devbox-search",
"version": "3.12.1",
"systems": {
"aarch64-darwin": {
"store_path": "/nix/store/s1mr5bmvfcy4hm7669d2xx3gqgj481qk-python3-3.12.1"
},
"aarch64-linux": {
"store_path": "/nix/store/qv710q1p74qb7bswpwh59px28gfj51b1-python3-3.12.1"
},
"x86_64-darwin": {
"store_path": "/nix/store/051hdrw5qmgbplch184mplcnv4djfb8a-python3-3.12.1"
},
"x86_64-linux": {
"store_path": "/nix/store/y4dxr00qg40pwgxx9nxj61091zk8bsvl-python3-3.12.1"
}
}
},
"shellcheck@latest": {
"last_modified": "2023-10-25T20:49:13Z",
"resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#shellcheck",
"source": "devbox-search",
"version": "0.9.0",
"systems": {
"aarch64-darwin": {
"store_path": "/nix/store/w3i70rjj7nhz211hp172g5i6f8f7kp1j-shellcheck-0.9.0-bin"
},
"aarch64-linux": {
"store_path": "/nix/store/4fapanq6k3x6pb990mbfcnj0cwzsizr7-shellcheck-0.9.0-bin"
},
"x86_64-darwin": {
"store_path": "/nix/store/gkv79r6iiys1k9d523slnz4k7vf99xcl-shellcheck-0.9.0-bin"
},
"x86_64-linux": {
"store_path": "/nix/store/sfc6nh6a2q1a71dz39rm4y5c7az1s3di-shellcheck-0.9.0-bin"
}
}
},
"shfmt@latest": {
"last_modified": "2023-10-25T20:49:13Z",
"resolved": "github:NixOS/nixpkgs/75a52265bda7fd25e06e3a67dee3f0354e73243c#shfmt",
"source": "devbox-search",
"version": "3.7.0",
"systems": {
"aarch64-darwin": {
"store_path": "/nix/store/3nfimx4z0nbxdkkshqpxzrh73llm57vk-shfmt-3.7.0"
},
"aarch64-linux": {
"store_path": "/nix/store/2qk1vvq18nfvjjf1rz4kdxbjd6rayw6a-shfmt-3.7.0"
},
"x86_64-darwin": {
"store_path": "/nix/store/1rigank4nlkq41dzqvp1di4hd628wp02-shfmt-3.7.0"
},
"x86_64-linux": {
"store_path": "/nix/store/lv02gfaqb60xl8a3x55g6s2s0yqrrpp5-shfmt-3.7.0"
}
}
}
}
}

121
docs/CC0 Normal file
View file

@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

325
docs/changelog.md Normal file
View file

@ -0,0 +1,325 @@
# changelog
## surplus v2024.0.0
(unreleased)
!!! information
this is a tentative release. surplus is currently versioned as `2024.0.0-beta`, as its
behaviour is not stabilized
!!! warning
this is an api-breaking release. see 'the great api break'.
command-line usage of surplus has not changed
### what's new
- added flag `--show-user-agent`, printing the fingerprinted user agent string and exiting
### what's changed
- `default_geocoder()` and `default_reverser()` have been deprecated since v2.1.0 and are now
removed. use the `SurplusDefaultGeocoding` class instead
- `SurplusException` is now `SurplusError`
### the great api break
TODO
### thanks!
- [vlshields](https://github.com/vlshields/) for their support with a drink!
full changelog: <https://github.com/markjoshwel/surplus/compare/v2.2.0...v2024.0.0>
## surplus on wheels v2
(released on the 1st of July 2024 on tag `v2.2024.25+spow`)
### changes
- you can now customize command invocations with `SURPLUS_CMD` and `LOCATION_CMD` environment variables
- surplus on wheels will purge logs when setting the `SPOW_PRIVATE` environment flag
### thanks!
- [vlshields](https://github.com/vlshields/) for their support with a drink!
---
## surplus on wheels: WhatsApp Bridge v2.2024.25
(released on the 17th of June 2024 on tag `v2.2024.25+spow-whatsapp-bridge`)
!!! note
from henceforth, the WhatsApp Bridge is now versioned with a modified calendar versioning scheme
of `MAJOR.YEAR.ISOWEEK`, where the `MAJOR` version segment will be bumped with codebase changes,
whereas the `YEAR` and `ISOWEEK` segments will represent the time of which the release was
built at
### changes
- updated dependencies to latest versions
- added `pair-phone` and `reconnect` subcommands
- TODO added optional helper script to auto-update to newer versions via a user-made daily cron job
### thanks!
- [vlshields](https://github.com/vlshields/) for their support with a drink!
---
## surplus on wheels: Telegram Bridge v2.2024.25
(released on the 17th of June 2024 on tag `v2.2024.25+spow-telegram-bridge`)
!!! note
from henceforth, the Telegram Bridge will automatically release a new version once a week if
there are updates to its dependencies
as such, the bridge is now versioned with a modified calendar versioning scheme of
`MAJOR.YEAR.ISOWEEK`, where the `MAJOR` version segment will be bumped with codebase changes,
whereas the `YEAR` and `ISOWEEK` segments will represent the time of which the release was
built at
### changes
- updated dependencies to latest versions
- added `logout` subcommand
- TODO added optional helper script to auto-update to newer versions via a user-made daily cron job
### thanks!
- [vlshields](https://github.com/vlshields/) for their support with a drink!
---
## surplus on wheels v1
initial release on the 9th of November 2023
---
## surplus on wheels: WhatsApp Bridge v1
initial release on the 7th of November 2023
---
## surplus on wheels: Telegram Bridge v1
initial release on the 7th of November 2023
---
## surplus v2.2.0
(released on the 14th of October 2023)
!!! warning
constants are changed in this update!
fixed a bug installing surplus on Python 3.12 and italian sharetext fixes
### what's new
- special key arrangements for malaysia
- support for termux-location json input
### what's fixed
- fixed typing-extensions as an unwritten dependency
this also fixes a bug in not being able to run surplus in Python 3.12
- fixed italian key arrangements [#34](https://github.com/markjoshwel/surplus/pull/34)
### what's changed
- **`SHAREABLE*` constants are now dictionaries, see api docs for more information**
<https://github.com/markjoshwel/surplus/compare/v2.1.1...v2.2.0>
---
## surplus v2.1.1
(released on the 19th of September 2023)
fix roads not coming first in Italian addresses (#31)
- documentation enhancements
- remove self in `SurplusReverserProtocol` conforming signature
- fix mismatching carets and add info on `split_iso3166_2`
- alternative line 3 arrangement for IT/Italy in [#31](https://github.com/markjoshwel/surplus/pull/31)
<https://github.com/markjoshwel/surplus/compare/v2.1.0...v2.1.1>
---
## surplus v2.1.0
(released on the 6th of September 2023)
!!! warning
there are backwards-compatible api changes in this release.
type-to-type location representation conversions and quality of life changes/fixes
- **`default_geocoder()` and `default_reverser()` functions have been deprecated in favour of the
new [`SurplusDefaultGeocoding` class](https://github.com/markjoshwel/surplus/tree/main#class-surplusdefaultgeocoding)**
- add reading from stdin when query is "-" in [#23](https://github.com/markjoshwel/surplus/pull/23)
- type to type conversion in [#24](https://github.com/markjoshwel/surplus/pull/24)
- fix local codes not being recognised if split with comma in [#29](https://github.com/markjoshwel/surplus/pull/29)
- more verbose -v/--version information in [#21](https://github.com/markjoshwel/surplus/pull/21)
<https://github.com/markjoshwel/surplus/compare/v2.0.1...v2.1.0>
---
## surplus v2.0.1
(released on the 5th of September 2023)
- expose surplus.Result in `__init__.py` by in [#28](https://github.com/markjoshwel/surplus/pull/28)
<https://github.com/markjoshwel/surplus/compare/v2.0.0...v2.0.1>
---
## surplus v2.0.0
(released on the 3rd of September 2023)
!!! warning
this is an api-breaking release. see 'the great api break'.
command-line usage of surplus has not changed
!!! information
python 3.11 or later is required due to a bug in earlier versions
[(python/cpython#88089)](https://github.com/python/cpython/issues/88089)
complete rewrite and string query support
### changes
- surplus has been fully rewritten in [#19](https://github.com/markjoshwel/surplus/pull/19)
- support for string queries
```text
$ s+ Wisma Atria
surplus version 2.0.0
Wisma Atria
435 Orchard Road
238877
Central, Singapore
```
- mypy will now recognise surplus as a typed module
- **python 3.11 is now the minimum version**
### the great api break
#### what is new
- nominatim keys are now stored in tuple constants
- surplus exception classes are now a thing
- surplus functions now operate using a unified `Behaviour` object
- surplus functions now return a `Result` object for safer value retrieval instead of the previous
`(bool, value)` tuple
- dedicated NamedTuple classes for each query type
#### what has been removed
- `surplus.handle_query()`
instead, use `.to_lat_long_coord()` on your surplus 2.x query object
#### what has remained
- `surplus.surplus()`, the function
- `surplus.parse_query()`, the function
#### what has changed
- `surplus.surplus()`
1. `reverser` and `debug` arguments are now under the unified `surplus.Behaviour` object
2. function now returns a `surplus.Result[str]` for safer error handling
- `surplus.parse_query()`
1. `query` and `debug` arguments are now under the unified `surplus.Behaviour` object
2. function now returns a `surplus.Result[surplus.Query]` for safer error handling
- `surplus.Latlong`
attributes `lat` and `long` have been renamed to `latitude` and `longitude` respectively
- `surplus.Localcode`
renamed to `surplus.LocalCodeQuery`
- `Localcode.full_length()`
renamed to `LocalCodeQuery.to_full_plus_code()`, and returns a `surplus.Result[str]` for safer
error handling
full changelog: <https://github.com/markjoshwel/surplus/compare/v1.1.3...v2.0.0>
## surplus v1.1.3
(released on the 21st of June 2023)
general output fixes and quality of life updates
- ci(qc) workflow tweaks by [markjoshwel](https://github.com/markjoshwel) in [#13](https://github.com/markjoshwel/surplus/pull/13)
- cc: remove woodlands test + brackets by [markjoshwel](https://github.com/markjoshwel) in [#14](https://github.com/markjoshwel/surplus/pull/14)
- s+: display county before state by [markjoshwel](https://github.com/markjoshwel) in [#15](https://github.com/markjoshwel/surplus/pull/15)
<https://github.com/markjoshwel/surplus/compare/v1.1.2...v1.1.3>
---
## surplus v1.1.2
(released on the 18th of June 2023)
general output fixes and quality of life updates
- do not repeat details by [markjoshwel](https://github.com/markjoshwel) in #9
- add -v/--version flag by [markjoshwel](https://github.com/markjoshwel) in #11
<https://github.com/markjoshwel/surplus/compare/v1.1.0...v1.1.1>
---
## surplus v1.1.1
(released on the 16th of June 2023)
### changes
fixes and output tweaks
- fix reverser returning a None location by [shamsu07](https://github.com/shamsu07) in #5
### thanks!
- [shamsu07](https://github.com/shamsu07) made their first contribution!
<https://github.com/markjoshwel/surplus/compare/v1.1.0...v1.1.1>
---
## surplus v1.1.0
(released on the 3rd of June 2023)
short code and latitude longitude coordinate pair support!
- code: s+ alternative shorthand script
- code: handle none/list locations
- code: query by lat long support
- code: support shortcodes with localities
- code: implement more address detail tags from nominatim
- meta: slsa 3 compliance
<https://github.com/markjoshwel/surplus/compare/v1.0.0...v1.1.0>
---
## surplus v1.0.0
initial release on the 2nd of June 2023

80
docs/contributing.md Normal file
View file

@ -0,0 +1,80 @@
# the contributor's handbook
expected details on development workflows? see [the developer's handbook](developing.md)
## which forge do i use?
as at the time of writing this documentation, i am actively using both
<https://github.com/markjoshwel/surplus> and <https://forge.joshwel.co/mark/surplus>
use whatever is more comfortable to you. do you not like microsoft and/or have moved away from github?
feel free to use <https://forge.joshwel.co>. don't want to make an account for either? did the forge
implode and is down? okay! mail in a git patch at <mark@joshwel.co>
## git workflow
1. fork the repository and branch off from the `future` branch,
or `main` if not available
2. make and commit your changes!
3. pull in any changes from upstream, and resolve any conflicts, if any
4. if needed, **commit your copyright waiver** (_see [waiving copyright](#waiving-copyright)_)
5. submit a pull request (_or mail in a patch_)
### waiving copyright
!!! danger "Warning"
this section is a **must** to follow if you have modified **any** unlicenced code:
- top-level surplus files (`releaser.py`, etc)
- surplus (`src/surplus`)
- surplus Documentation (`docs/`)
- surplus on wheels (`src/surplus-on-wheels`)
- surplus on wheels: Telegram Bridge (`src/spow-telegram-bridge`)
!!! info
the command to create an empty commit is `git commit --allow-empty`
when contributing your first changes, please include an empty commit for a copyright
waiver using the following message (replace `Your Name` with your name or username):
```text
Your Name Copyright Waiver
I dedicate any and all copyright interest in this software to the
public domain. I make this dedication for the benefit of the public at
large and to the detriment of my heirs and successors. I intend this
dedication to be an overt act of relinquishment in perpetuity of all
present and future rights to this software under copyright law.
To the best of my knowledge and belief, my contributions are either
originally authored by me or are derived from prior works which I have
verified are also in the public domain and are not subject to claims
of copyright by other parties.
To the best of my knowledge and belief, no individual, business,
organization, government, or other entity has any copyright interest
in my contributions, and I affirm that I will not make contributions
that are otherwise encumbered.
```
(from <https://unlicense.org/WAIVER>)
for documentation contributors, if you have contributed a
[legally significant](https://www.gnu.org/prep/maintain/maintain.html#Legally-Significant) or have
repeatedly commited multiple small changes, waive your copyright with the CC0 deed
(replace `Your Name` with your name or username):
```text
Your Name Copyright Waiver
The person who associated a work with this deed has dedicated the work to
the public domain by waiving all of his or her rights to the work worldwide
under copyright law, including all related and neighboring rights, to the
extent allowed by law.
```
(from <https://creativecommons.org/publicdomain/zero/1.0/>)
## reporting incorrect output
TODO

337
docs/developing.md Normal file
View file

@ -0,0 +1,337 @@
# the developers handbook
!!! abstract
i (mark), heavily use nix to manage my projects, either with
[devbox](https://github.com/jetpack-io/devbox) or flakes
if you are going to develop for surplus or its' sibling projects (except surplus on wheels,
which only needs `shfmt` and `shellcheck`), i would recommend you install Nix using
[Determinate Systems' Nix Installer](https://github.com/DeterminateSystems/nix-installer):
```text
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
if i, a very very inexperienced Nix and NixOS user to give a rundown on why to use it:
1. **environments and builds are reproducible**
nix is part package manager, operating system (as NixOS), and functional programming
language. because it's functional, having X as an input will always produce Y as an output,
no matter what. even in the event of environmental, social, economic, or structural collapse
**what does this mean for you?**
when i use `nix develop` and you use `nix develop` to start a development environment, your
version of python will be the same as my version of python. your version of go will be same
as my version of go, etc
if i can build it, and the locked inputs of the nix flake are still available on the internet,
you can build it too
2. **the nix store**, located literally at `/nix/store`
it's where it stores every package you need, separate and isolated from other packages.
lets say you have a tool that needs python (3.8), and another tool that needs python (3.11).
nix store will download and store the binaries for both python installations, instead of
sharing the earliest downloaded python version for both tools
**what does this mean for you?**
whatever project you're working on that can use nix for development environments and builds
will not dirty anything else on your system. any build dependencies of surplus provided with
`nix develop` will **not** mess up software installed for other projects or even the system.
neat, if you ask me tbh
**tl;dr**: things will just werk with nix. but if you see all of this and go, "eh. i can manage
what i install.", then power to you! i list down exactly what prerequisite software needs to be
installed for each project anyways, so have fun! (●'◡'●)
## surplus (and Documentation)
### environment setup
!!! note
all prerequisite software are available in a nix flake. if you want a reproducible environment,
run `nix develop --impure` in the repository root
- for NixOS users, [nix-ld](https://github.com/Mic92/nix-ld) is needed. if you use flakes to
declare your system, follow accordingly. else, use `/etc/nixos/configuration.nix`
for NixOS-on-WSL users, use [nix-ld-rs](https://github.com/nix-community/nix-ld-rs)
once you're done installing `nix-ld` or `nix-ld-rs`,
don't forget to run `sudo nixos-rebuild switch`
prerequisite software:
- [Python](https://www.python.org/downloads/), 3.11 or newer
- [Hatch](https://hatch.pypa.io/latest/): dependency management and build tool
to start a development environment:
```text
hatch shell
```
for docs:
```text
hatch -e docs shell
```
### workflow for python code
TODO
### workflow for markdown documentation
run the documentation server with:
```text
hatch run docs:serve
```
i personally don't use a linter for markdown files, if it looks good on my code editor, then
whatever. if you're going to contribute back, i ask for three things:
- run it through a spell checker or something similar
- line limit of 100
- should be readable as-is on a code editor, **not the markdown preview pane**.
my stance is, if you can afford a fancy preview of the markdown file, use the nice-ned
documentation website. else, read it as a plaintext file
(make it look pretty on the doc site and in plaintext)
---
## surplus on wheels
### environment setup
!!! note
all prerequisite software are available in a nix flake. if you want a reproducible environment,
run `nix develop` in `src/surplus-on-wheels`
prerequisite software:
- [shfmt](https://github.com/patrickvane/shfmt): formatter
- [ShellCheck](https://www.shellcheck.net/): static analyser
### workflow
!!! note
alternatively, run `check.sh` inside `src/surplus-on-wheels`
- formatting s+ow:
- run `shfmt s+ow > s+ow.new`
- mv `s+ow.new` into `s+ow`
sometimes when piping shfmt's output immediately into the same file
results in the file being empty :(
- checking s+ow:
- run `shellcheck s+ow`
if there's no output, that means it passed :)
- if commiting back into the repository, try it out on your Termux system for a day or two,
just to make sure it runs correctly
---
## surplus on wheels: Telegram Bridge
### environment setup
!!! note
all prerequisite software are available in a nix flake. if you want a reproducible environment,
run `nix develop` in `src/spow-telegram-bridge`. it uses
[poetry2nix](https://github.com/nix-community/poetry2nix), so you won't need to run
`poetry shell` afterwards. if you've changed the `pyproject.toml` file,
just exit and re-run `nix develop`
prerequisite software:
- [Python](https://www.python.org/downloads/), 3.11 or newer
- [Poetry](https://python-poetry.org/): dependency management and build tool
to start a development environment:
```text
poetry shell
```
### workflow
after modifying,
1. check the source code:
1. `mypy bridge.py`
2. `ruff format bridge.py`
3. `ruff check bridge.py`
!!! note
alternatively, run `check.sh` inside `src/spow-telegram-bridge`
2. and then [test the binary](#workflow-for-testing-the-binary)
if the bridge behaves nominally, [bump the version](#versioning-surplus-on-wheels-telegram-bridge)
and commit!
---
## surplus on wheels: WhatsApp Bridge
### environment setup
!!! note
all prerequisite software are available in a nix flake. if you want a reproducible environment,
run `nix develop` in `src/spow-whatsapp-bridge`
the flake will pull in the Android SDK and NDK for building on Termux, and as such can only be
ran on `x86_64-linux` and `x86_64-darwin`
prerequisite software:
- [Go](https://go.dev): 1.22 or newer
- [Android NDK](https://developer.android.com/ndk/downloads), if building for Termux
### workflow for modifying bridge code
the bridge's code is just modified [mdtest](https://github.com/tulir/whatsmeow/tree/main/mdtest)
code, and as such, whenever in doubt, do a diff between mdtest and the bridge code
after modifying,
1. check the source code:
1. `go fmt bridge.go`
2. `go vet bridge.go`
3. `golint bridge.go`
!!! note
alternatively, run `check.sh` inside `src/spow-whatsapp-bridge`
2. [build a binary](#workflow-for-building-a-binary)
3. [test the binary](#workflow-for-testing-the-binary)
4. and if all goes well, [bump the version](#versioning-surplus-on-wheels-whatsapp-bridge)
and commit!
### workflow for bumping dependencies
- check with your editor, plugin, or online if there's newer patch/minor (see
[semantic versioning](https://semver.org/)) versions to update to
- change the `go.mod` accordingly
after bumping,
1. [build a binary](#workflow-for-building-a-binary)
2. [test the binary](#workflow-for-testing-the-binary)
3. and if all goes well, [bump the version](#versioning-surplus-on-wheels-whatsapp-bridge)
and commit!
### workflow for building a binary
ensure you already have c compiler on the system (if you're using `nix develop` then yes you do), then run:
```text
CGO_ENABLED=1 go build
```
nix users can alternatively run:
```text
nix build
```
instructions to build a Termux build are located at the
[bridges' documentation page](onwheels/whatsapp-bridge.md#anywhere-else), however nix users can run
the following instead for a reproducible, deterministic and hermetic build command:
```text
nix build .#termux
```
the resulting build will be in `result/spow-whatsapp-bridge`
### workflow for testing the binary
- test it out, making sure that you write dummy test text to `~/.cache/s+ow/message` before running
the binary
1. run `s+ow-whatsapp-bridge login` first
2. run `s+ow-whatsapp-bridge list` if you don't already have a chat ID
to send the test message to
3. run `s+ow-whatsapp-bridge` type or copy and paste in a `wa:`-prefixed chat ID
after it logs in, and verify it sends
if the bridge behaves nominally, [bump the version](#versioning-surplus-on-wheels-whatsapp-bridge)
and commit!
## workflow for versioning and tagging releases
### versioning surplus
format: `YEAR.MAJOR.MINOR[-PRERELEASE]` ([semantic versioning](https://semver.org/))
example: `2024.0.0`, `2024.0.0-beta`
change: update the `__version__` variable in `src/surplus/surplus.py`
### versioning surplus on wheels
i've tried to make surplus on wheels as reliable as it could be given a POSIX compliant shell and
commands you'd find available on virtually every linux system, Termux included
as such, it doesn't really follow a versioning scheme as it doesn't need to. also there's no
automatic updater for it, which would be overkill anyway
### versioning surplus on wheels: Telegram Bridge
format: `REVISION.YYYY.WW[+BUILD]` ([calendar versioning](https://calver.org/))
example: `2.2024.24`, `2.2024.24+1`
change: `version` key in `src/spow-telegram-bridge/pyproject.toml`
`REVISION` here meaning any general revision/change
the Telegram Bridge relies on [Telethon](https://github.com/LonamiWebs/Telethon/), which also
follows [semantic versioning](https://semver.org/). so, as long as major isn't bumped, or
as long as Telegram doesn't become Discord, the MTProto APIs to talk to Telegram should be
stable.
however because Telethon also relies on a bunch of networking libraries, it made some sense to
still do weekly builds to bump dependencies, getting pipx to download the newest compatible
dependencies as compared to dubiously running some sort of script to `pipx inject` dependencies
under normal circumstances, a non-working version of the bridge would and **should not have a
version bump**. but for any reason if an already tagged bridge is faulty and/or erroneous in
normal/expected usage, add a revision number to the end after a period (see example above)
### versioning surplus on wheels: WhatsApp Bridge
format: `REVISION.YYYY.WW[+BUILD]` ([calendar versioning](https://calver.org/))
example: `2.2024.25`, `2.2024.25+1`
change: `version` attribute of `bridge` attribute set in `src/spow-whatsapp-bridge/flake.nix`
`REVISION` here meaning any general revision/change
the WhatsApp Bridge relies on [whatsmeow](https://github.com/tulir/whatsmeow), a rolling release
library due to the volatile, undocumented nature of WhatsApp's multidevice API and also directly
and indirectly relies on a bunch of networking libraries:
``` title="src/spow-whatsapp-bridge/go.mod"
--8<-- "src/spow-whatsapp-bridge/go.mod"
```
as such, it uses a calendar versioning scheme and is built weekly
under normal circumstances, a non-working version of the bridge would and **should not have a
version bump**. but for any reason if an already tagged bridge is faulty and/or erroneous in
normal/expected usage, add a revision number to the end after a period (see example above)
---
## i've made my changes. what now?
if you're contributing back to surplus and/or the sibling projects, firstly, thanks!
see [the contributor's handbook](contributing.md) for what's next

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
docs/fonts/GeistVF.woff2 Normal file

Binary file not shown.

92
docs/fonts/LICENSE.txt Normal file
View file

@ -0,0 +1,92 @@
Copyright (c) 2023 Vercel, in collaboration with basement.studio
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

42
docs/index.md Normal file
View file

@ -0,0 +1,42 @@
# surplus (s+)
surplus (s+) is a Python script to convert [Google Maps Plus Codes](https://maps.google.com/pluscodes/)
to iOS Shortcuts-like shareable text
!!! tip
termux users can consider [surplus on wheels](onwheels/index.md), a sibling project that allows
you to run surplus regularly throughout the day and send it to someone on a messaging platform
!!! important
python 3.11 or later is required due to a bug in earlier versions
[(python/cpython#88089)](https://github.com/python/cpython/issues/88089)
install surplus with pip, or [pipx](https://pipx.pypa.io/) (recommended):
```text
pipx install surplus
```
then, use the `surplus` command, or its `s+` shorthand:
```text
$ s+ 7RGX+GJ Singapore
surplus version 2024.0.0
Singapore Conference Hall
7 Shenton Way
068809
Central, Singapore
```
the types of queries you can pass in are:
- full-length Plus Codes
`6PH58QMF+FX`
- shortened Plus Codes / 'local codes'
`8QMF+FX Singapore`
- latitude and longitude coordinate pairs
`1.3336875, 103.7749375`
- string queries
`Wisma Atria`
or, alternatively pass in `-` to read from stdin

113
docs/licences.md Normal file
View file

@ -0,0 +1,113 @@
# licences
## [surplus](index.md)
**The Unlicence**
surplus is free and unencumbered software released into the public domain:
``` title="src/surplus/UNLICENCE"
--8<-- "src/surplus/UNLICENCE"
```
however, the dependencies surplus relies on are licenced under different, but still permissive
and open-source licences:
- [**geopy**](https://pypi.org/project/geopy/) —
Python Geocoding Toolbox
MIT Licence
- [**geographiclib**](https://pypi.org/project/geographiclib/) —
The geodesic routines from GeographicLib
MIT Licence
- [**pluscodes**](https://pypi.org/project/pluscodes/) —
Compute Plus Codes (Open Location Codes)
Apache 2.0
---
## [surplus on wheels](onwheels/index.md)
**The Unlicence**
surplus on wheels is free and unencumbered software released into the public domain:
``` title="src/surplus-on-wheels/UNLICENCE"
--8<-- "src/surplus-on-wheels/UNLICENCE"
```
---
## [surplus on wheels: WhatsApp Bridge](onwheels/whatsapp-bridge.md)
**Mozilla Public Licence 2.0**
the s+ow WhatsApp Bridge is based off of mdtest code from the
[whatsmeow](https://github.com/tulir/whatsmeow) project, which is licenced under the Mozilla
Public Licence 2.0:
``` title="src/spow-whatsapp-bridge/LICENCE"
--8<-- "src/spow-whatsapp-bridge/LICENCE"
```
the direct dependencies s+ow-whatsapp-bridge relies on are licenced under different, but still
permissive and open-source licences:
- [**whatsmeow**](https://github.com/tulir/whatsmeow) —
Go library for the WhatsApp web multidevice API
Mozilla Public Licence 2.0
---
## [surplus on wheels: Telegram Bridge](onwheels/telegram-bridge.md)
**The Unlicence**
the s+ow Telegram Bridge is free and unencumbered software released into the public domain:
``` title="src/spow-telegram-bridge/UNLICENCE"
--8<-- "src/spow-telegram-bridge/UNLICENCE"
```
however, the direct dependencies surplus relies on are licenced under different, but still
permissive and open-source licences:
- [**Telethon**](https://pypi.org/project/Telethon/) —
Pure Python 3 MTProto API Telegram client library, for bots too!
MIT Licence
---
## [surplus documentation](index.md)
**CC0 1.0 Universal**
the textual contents of surplus documentation by [Mark Joshwel](https://joshwel.co) is marked
with [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/).
to view a copy of this licence, visit <https://creativecommons.org/publicdomain/zero/1.0/>
``` title="docs/CC0"
--8<-- "docs/CC0"
```
the fonts the documentation website relies on are licenced under different, but still
permissive and open-source licences:
- [**Geist and Geist Mono**](https://github.com/vercel/geist-font)
SIL Open Font Licence 1.1
``` title="docs/fonts/LICENSE.txt"
--8<-- "docs/fonts/LICENSE.txt"
```
the direct software dependencies the documentation are also licenced under different, but still
permissive and open-source licences:
- [**mkdocs-material**](https://squidfunk.github.io/mkdocs-material/) —
Documentation that simply works
MIT Licence
- [**mkdocs**](https://www.mkdocs.org/) —
Project documentation with Markdown
BSD-2-Clause Licence

69
docs/links.md Normal file
View file

@ -0,0 +1,69 @@
# backup links
for when first-party links like <https://surplus.joshwel.co> and <https://forge.joshwel.co> are down:
## surplus
``` title="Primary Link"
https://forge.joshwel.co/mark/surplus.git
```
``` title="Alternative Link"
https://github.com/markjoshwel/surplus.git
```
## surplus on wheels
- shell script
``` title="Primary Link"
https://surplus.joshwel.co/spow.sh
```
``` title="Alternative Link"
https://raw.githubusercontent.com/markjoshwel/surplus/main/src/surplus-on-wheels/s+ow
```
- termux installation script
``` title="Primary Link"
https://surplus.joshwel.co/termux.sh
```
``` title="Alternative Link"
https://raw.githubusercontent.com/markjoshwel/surplus/main/src/surplus-on-wheels/install.sh
```
## surplus on wheels: Telegram Bridge
- install/update script:
``` title="Primary Link"
https://surplus.joshwel.co/telegram.sh
```
``` title="Alternative Link"
https://raw.githubusercontent.com/markjoshwel/surplus/main/src/spow-telegram-bridge/install.sh
```
- pipx target
``` title="Primary Link"
git+https://forge.joshwel.co/mark/surplus.git#egg=spow-telegram-bridge&subdirectory=src/spow-telegram-bridge
```
``` title="Alternative Link"
git+https://github.com/markjoshwel/surplus.git#egg=spow-telegram-bridge&subdirectory=src/spow-telegram-bridge
```
## surplus on wheels: WhatsApp Bridge
- install/update script:
``` title="Primary Link"
https://surplus.joshwel.co/whatsapp.sh
```
``` title="Alternative Link"
https://raw.githubusercontent.com/markjoshwel/surplus/main/src/spow-whatsapp-bridge/install.sh
```

58
docs/onwheels/bridges.md Normal file
View file

@ -0,0 +1,58 @@
# surplus on wheel bridges
## official bridges
there are two currently “official” bridges:
- [surplus on wheels: WhatsApp Bridge](whatsapp-bridge.md)
- [surplus on wheels: Telegram Bridge](telegram-bridge.md)
## bring your own bridge
### an informal specification
s+ow bridges are relatively simple as they are:
1. an executable or script
2. that reads in `SPOW_TARGETS` given by surplus to the bridge, using the standard input (stdin)
stream
1. bridges do not need to account for the possibility of multiple lines sent to stdin
2. bridges should account for the possibility of comma and space (`", "` instead of just `","`)
delimited targets, and strip each target of preceding and trailing whitespace
3. bridges should recognise a platform based on a prefix
(e.g. `wa:` for WhatsApp, `tg:` for Telegram, etc.)
3. reads `SPOW_MESSAGE` (`~/.cache/spow/message`) for the message content
notes:
1. stderr and stdout are redirected to s+ows error and output logs respectively unless the
`-p / --private` flag is passed to surplus
2. any errors encountered by the bridge should always result in a non-zero return. error logs will
show the exact error code, so feel free to use other numbers than 1
3. persistent data such as credentials and session data storage are to be handled by the
bridge itself. consider storing them in `$HOME/.local/share/<bridge-name>/`, or wherever
appropriate
### example
if i were to recommend an example on a basic bridge implementation, it would be the
[Telegram Bridge](telegram-bridge.md):
```python title="src/spow-telegram-bridge/bridge.py"
--8<-- "src/spow-telegram-bridge/bridge.py"
```
!!! note
the feature of deleting the last sent message (`--delete-last`) is a non-standard feature for
bridges, and was simply a use case i personally needed. if you're going to implement a bridge,
all you really need is the ability to `login`, `logout`, and [send a message](#an-informal-specification)
you can add other features as per the needs of your platform, like how the WhatsApp Bridge has
a `pair-phone` subcommand, or per your use case needs, like in the Telegram Bridge's `--delete-last`.

View file

@ -0,0 +1,43 @@
# emulating `termux-location`
to bodge surplus on wheels (s+ow) on non-Termux systems
!!! note
dummy admonition for colour matching
!!! warning
dummy admonition for colour matching
`termux-location`, part of [Termux:API](https://wiki.termux.com/wiki/Termux:API), gets the device's
location via android apis and returns a json response through stdout:
```text
{
"latitude": 1.3277513,
"longitude": 103.678317,
"altitude": 51.6298828125,
"accuracy": 48.46337890625,
"vertical_accuracy": 38.4659423828125,
"bearing": 0.0,
"speed": 0.0,
"elapsedMs": 28,
"provider": "gps"
}
```
see <https://wiki.termux.com/wiki/Termux-location> for more information
## implementing for surplus on wheels (s+ow)
s+ow will call the command a total of six times, being three pairs of parallel
`$LOCATION_CMD -p "network"` and `$LOCATION_CMD -p "gps"` invocations, before deciding after
exhausting all six runs on which output to choose, if any command runs were successful
even if somewhere in the termux-location implementation fails, it always (begrudgingly) returns
zero. s+ow will treat the invocation of the command as successful if there is **any output** to
the standard output (stdout) stream
## implementing for surplus (s+)
s+, when passed `--t / --using-termux-location`, will consume stdin, parse it as json and then
attempt to retrieve the `latitude` and `longitude` keys as floating point numbers

View file

@ -0,0 +1,46 @@
# emulating `termux-notification`
to bodge surplus on wheels (s+ow) on non-Termux systems
`termux-notification`, part of [Termux:API](https://wiki.termux.com/wiki/Termux:API), sends out an
android notification
without `termux-notification`, s+ow will still run as it doesn't use `set -e` and very carefully
handles all command invocations, with `termux-notification` being the graceful exception\
however, if you would like to emulate it, make an executable globally reachable with the same name
s+ow uses the command as such:
```shell
termux-notification \
--priority "default" \
--title "surplus on wheels: No bridges" \
--content "No '$SPOW_BRIDGES' file; message is not sent."
```
```shell
termux-notification \
--priority "min" \
--ongoing \
--id "s+ow" \
--title "surplus on wheels" \
--content "s+ow has started running."
```
```shell
termux-notification \
--priority "min" \
--id "s+ow" \
--title "surplus on wheels" \
--content ...
```
```shell
termux-notification \
--priority "default" \
--title "surplus on wheels has errored" \
--content ...
```
see <https://wiki.termux.com/wiki/Termux-notification> for more information

329
docs/onwheels/index.md Normal file
View file

@ -0,0 +1,329 @@
# surplus on wheels (s+ow)
surplus on wheels is a pure shell script to get your location using
[termux-location](https://wiki.termux.com/wiki/Termux-location), process it through surplus, and
send it to messaging service or wherever using “bridges”
surplus was made to emulate sending your location through the iOS Shortcuts app, and surplus on
wheels complements it by running surplus automatically using a cron job.
(but using it manually also works!)
## installing
!!! important
s+ow is a Termux-first script, and will not work anywhere else unless you have
a utility that emulates [termux-location](https://wiki.termux.com/wiki/termux-location)
on `$PATH` alongside bridges that supports your platform
there are two notable ways to install s+ow:
1. [as a standalone script](#as-a-standalone-script)
2. or, [as a cron job](#as-a-cron-job)
there is also an [installation script](#using-installation-scripts) for quickly getting
started from a _fresh_ termux installation
### as a standalone script
1. firstly install python and termux-api if you haven't already:
```text
pkg install python termux-api
```
also install the accompanying Termux:API app from [F-Froid](https://f-droid.org/en/packages/com.termux.api/)
2. install pipx if you haven't already:
```text
pip install pipx
```
3. install surplus:
```text
pipx install surplus
```
4. install surplus on wheels:
```text
mkdir -p ~/.local/bin/
wget -O ~/.local/bin/s+ow https://surplus.joshwel.co/spow.sh
chmod +x ~/.local/bin/s+ow
```
!!! note
if `wget` throws a 404, see [backup links](../links.md)
if `~/.local/bin` is not in your `$PATH`, add the following to your shell's rc file:
```shell
export PATH="$HOME/.local/bin:$PATH"
```
et voilà! s+ow is now setup. to actually send the message to a messaging platform,
[install an appropriate bridge](bridges.md)
### as a cron job
!!! important
these instructions rely on following the [previous instructions](#as-a-standalone-script)
1. install necessary packages to run cron jobs:
```text
pkg install cronie termux-services
```
2. restart termux and start the cron service:
```text
sv-enable cron
```
3. set up the cron job:
run the following command:
```text
crontab -e
```
and add the following text:
```text
59 * * * * bash -l -c "(SPOW_TARGETS="" SPOW_CRON=y s+ow)"
```
!!! important
minimally fill in the `SPOW_TARGETS` variable before running s+ow.
[(see usage for more info)](#usage)
this will run s+ow every hour, a minute before the hour
modify the variables as per your needs.
see [usage](#usage) for more information
et voilà! s+ow will now send a message every hour. feel free to experiment with the cron
job to your liking. see [crontab.guru](https://crontab.guru/) if youre new to cron jobs
if you havent already, [install an appropriate bridge](bridges.md) to actually send a
message to a messaging platform
### using installation scripts
!!! warning
these scripts assume you're starting from a fresh base installation of Termux.
if you have already cron jobs, then manually carry out the instructions in
'[as a cron job](#as-a-cron-job)'
!!! important
if not installed already, install
[Termux:API from F-Droid](https://f-droid.org/en/packages/com.termux.api/), **not the Play Store**
1. setup s+ow:
```text
wget -O- https://surplus.joshwel.co/termux.sh | sh
```
!!! note
if `wget` throws a 404, see [backup links](../links.md)
2. restart termux!
3. and finally, [set up a cron job](#as-a-cron-job) from step 3 onwards ('set up the cron job')
## usage
### environment variables
s+ow's behaviour can be customised environment variables, with `SURPLUS_CMD` being the only
required variable:
1. `SPOW_TARGETS`
a single line of comma-delimited chat IDs with bridge prefixes
```text
wa:000000000000000000@g.us,tg:-0000000000000000000,...
```
in the example above, the WhatsApp chat ID is `wa:`-prefixed as recognised by the
[spow-whatsapp-bridge](whatsapp-bridge.md), and the
Telegram chat ID is `tg:`-prefixed as recognised by the
[spow-telegram-bridge](telegram-bridge.md)
2. `SPOW_CRON` (optional)
set as non-empty to declare that s+ow is being run as a cron job
if running as a cron job, start s+ow one minute earlier than intended to account for the time
it takes to run `termux-location` and `surplus`. s+ow assumes this and delays itself
appropriately
setting it to `n` will also be treated as if it were empty
3. `SPOW_PRIVATE` (optional)
set as non-empty to discard all logs when s+ow is done:
- `$HOME/.cache/s+ow/out.log` will be set to `/dev/null`
- `$HOME/.cache/s+ow/err.log` will be set to `/dev/null`
- `$HOME/.cache/s+ow/location.net.json` will be cleared after use locating the device
- `$HOME/.cache/s+ow/location.gps.json` will be cleared after use locating the device
- `$HOME/.cache/s+ow/location.json` will be cleared after use locating the device
- `$HOME/.cache/s+ow/surplus.out.log` will be cleared after use generating the message
- `$HOME/.cache/s+ow/surplus.err.log` will be set to `/dev/null`
- `$HOME/.cache/s+ow/message` will be cleared after all bridges has sent the message
!!! warning
the only file not cleared is s+ow's last successful message file, `$HOME/.cache/s+ow/last`,
as s+ow uses this as the first fallback message if it couldn't locate the device in time.
if you're fine with using the `LOCATION_FALLBACK` string, feel free to modify your
cron job to remove this file after running s+ow
setting it to `n` will also be treated as if it were empty
4. `SURPLUS_CMD` (optional)
the custom invocation used when calling surplus, modify this if you want to add certain flags
this defaults to `surplus -td`
!!! warning
when overriding, ensure you also have `-td` (`--using-termux-location` and `--debug`) in
your custom invocation!
5. `LOCATION_CMD` (optional)
the custom invocation used when calling `termux-location`, modify this if you want to bodge
together surplus on wheels on non-termux systems.
see ([emulating `termux-location`](emulating-termux-location.md)) for more information
this defaults to `termux-location`
6. `LOCATION_PRIORITISE_NETWORK` (optional)
set as non-empty to declare that s+ow can just use network location instead of GPS
if GPS is taking too long.
you should only turn this on if punctuality means that much to you, or youre in a
country with cell towers close by or everywhere, like Singapore
the JIDs can be obtained by sending a message to the user/group, while running
`s+ow mdtest`, and examining the output for your message. JIDs are email address-like
strings
setting it to `n` will also be treated as if it were empty
7. `LOCATION_TIMEOUT` (optional)
set as a number to override the default first location timeout of `50`
8. `LOCATION_FALLBACK` (optional)
a string that can be formatted with three numbers using `%d`:
1. s+ow's status
2. number of location attempts before giving up
3. type of message sent
see [details on notification numbers](#details-on-notification-numbers) for the meanings of
each number. 'a', 'b' and 'c' map to `A`, `B` and `C`
defaults to `%d%d%d?`
### faking locations
> sometimes you gotta do what you gotta do
you can fake your s+ow messages by either:
1. setting a dummy `last` file in s+ow cache
`$HOME/.cache/s+ow/last` is used as the fallback response when a part of s+ow (either
`termux-location` or `surplus` errors out). you can set this file to whatever you want
and just turn off location on your device
2. setting a `fake` file in s+ow cache
!!! warning
s+ow uses the `read` command to read the file. as such, it is possible for s+ow to
prematurely stop reading the file if the file does not contain a trailing newline.
you can also write text to `$HOME/.cache/s+ow/fake` to fake upcoming messages. the file
is delimited by empty lines. as such, arrange the file like so:
```text
The Clementi Mall
3155 Commonwealth Avenue West
Westpeak Terrace
129588
Southwest, Singapore
Westgate
3 Gateway Drive
Jurong East
608532
Southwest, Singapore
...
```
on every run of s+ow, the first group of lines will be consumed, and the file will be
updated with the remaining lines. if the file is empty, it will be deleted
### details on notification numbers
after each run, or if s+ow had to use a location fallback string, s+ow notifies you:
!!! abstract "surplus on wheels"
Run has finished.
Singapore Conference Hall
7 Shenton Way
068809
Central, Singapore
(A, B, C, D<E>)
[lc:W sp:X sm:Y - Z]
!!! abstract "surplus on wheels has errored"
(A, B, C, D<E>)
[lc:W sp:X sm:Y - Z]
the top line denotes general statuses:
- `A`: s+ow's status
- `0` is nominal
- `1` is a termux-location error
- `2` is a surplus error
- `3` is a bridge/message send error
- `B`: number of location attempts before giving up
- `C`: type of message sent
- `0` for freshly made sharetext
- `1` for recycling a previous successful location sharetext (`last` file)
- `2` for using fallback template
- `D`: number of bridge failures
- `E`: each bridge's return code
the bottom line details on how long s+ow spent on each stage:
- `W`: time to locate
- `X`: time to run surplus
- `Y`: time to send message(s)
- `Z`: total run time
## help! a bridge isn't working!
cool. do the following:
1. log out and log back in and try again
2. if that didn't fix it, update/reinstall the bridge and try again
3. run the bridge's executable directly to see if there's any connection issues
look at your bridge's installation instructions to find out where it's located at.
or, use the `which` command
4. if it connected successfully, or you see no errors, try typing in one of the targets you've set
in `SPOW_TARGETS` for the bridge, and then press the enter/return key
!!! failure
on the off chance you reinstalled the bridge, and it still failed either step 3 or 4, the bridge
itself is faulty. file a bug report/issue with the bridge's project page or maintainer and tell
them where it failed (was it connecting to the messaging service? or failure to send a message?)

View file

@ -0,0 +1,101 @@
# surplus on wheels: Telegram Bridge
Telegram Bridge for surplus on wheels (s+ow)
s+ow bridges are defined in a file named `$HOME/.s+ow-bridges`. each command in the file is run,
and comma-seperated target chat IDs are passed using stdin.
this bridge recognises targets prefixed with `tg:`.
```text
tg:<chat id>,...
```
## installation
!!! important
the following instructions implies that [surplus](../index.md) and [surplus on wheels](bridges.md)
have already been installed
1. install prerequisite software if not installed:
```text
pkg install git
```
```text
pip install pipx
```
2. install spow-telegram-bridge:
```text
wget -O- https://surplus.joshwel.co/telegram.sh | sh
```
!!! note
if `wget` throws a 404, see [backup links](../links.md)
3. add the following to your `$HOME/.s+ow-bridges` file:
```text
SPOW_TELEGRAM_API_HASH="" SPOW_TELEGRAM_API_ID="" s+ow-telegram-bridge
```
fill in SPOW_TELEGRAM_API_HASH and SPOW_TELEGRAM_API_ID accordingly.
see the [Telethon docs](https://docs.telethon.dev/en/stable/basic/signing-in.html) for
more information
to keep up to date, look at [updating](#updating) to set up a daily update cron job:
## updating
the installation script also sets up a shell script under the `s+ow-telegram-bridge-update` command
```text
s+ow-telegram-bridge-update
```
to do this automatically, make a cron job with `crontab -e`
and make a new line with the following text:
```text
0 0 * * * bash -l -c "s+ow-telegram-bridge-update"
```
this cron job will run the command every day at midnight
## usage
- `s+ow-telegram-bridge`
normal usage; sends latest message to tg:-prefixed targets given in stdin
- `s+ow-telegram-bridge login`
logs in to Telegram
- `s+ow-telegram-bridge logout`
logs out of Telegram
- `s+ow-telegram-bridge list`
lists all chats and their IDs
optional arguments:
- `--silent`
asks telegram to send message silently
- `--delete-last`
deletes last location message to prevent clutter
## versioning scheme
from `v2.2024.27`, the Telegram Bridge will automatically release a new version once a week if there
are updates to its dependencies
as such, the bridge is now versioned with a modified calendar versioning scheme of
`MAJOR.YEAR.ISOWEEK`, where the `MAJOR` version segment will be bumped with codebase changes, whereas
the `YEAR` and `ISOWEEK` segments will represent the time of which the release was built at
## licence
the s+ow Telegram Bridge is free and unencumbered software released into the public domain.
for more information, see [licences](../licences.md).

View file

@ -0,0 +1,210 @@
# surplus on wheels: WhatsApp Bridge
WhatsApp Bridge for surplus on wheels (s+ow)
s+ow bridges are defined in a file named `$HOME/.s+ow-bridges`. each command in the file is run,
and comma-seperated target chat IDs are passed using stdin.
this bridge recognises targets prefixed with `wa:`.
```text
wa:<chat id>,...
```
## installation
### from a pre-built binary
```text
wget -O- https://surplus.joshwel.co/whatsapp.sh | sh
```
!!! note
if `wget` throws a 404, see [backup links](../links.md)
### building from source
#### on Termux
1. clone the repository at either `https://forge.joshwel.co/mark/surplus` or
`https://github.com/markjoshwel/surplus`, and navigate to `src/spow-whatsapp-bridge` within the
cloned repository
```text
git clone https://forge.joshwel.co/mark/surplus
cd surplus/src/spow-whatsapp-bridge
```
2. build the bridge:
```text
go build
```
for compatibility with the documentations' instructions as-is, rename the built binary to
`s+ow-whatsapp-bridge`
```text
mv spow-whatsapp-bridge s+ow-whatsapp-bridge
```
3. send the built binary over to your Termux environment, and then move it into the
`$HOME/.local/bin/` folder. if it doesn't exist, make it with `mkdir` and ensure that the folder
is in your `PATH` variable either using your `.profile`, `.bashrc` or whatever file is sourced
when opening your shell
#### anywhere else
for usage on Termux, see if the [Android NDK](https://developer.android.com/ndk/downloads) supports your platform
1. grab a copy of the NDK, and extract it somewhere. navigate to
`<ndk folder>/toolchains/llvm/prebuilt/<your platform>/bin` and look for a suitable `clang`
executable, as it will be your CGO compiler
```
m@csp:~/android-ndk-r26d/toolchains/llvm/prebuilt/linux-x86_64/bin$ ls *clang
aarch64-linux-android21-clang aarch64-linux-android30-clang ...
aarch64-linux-android22-clang aarch64-linux-android31-clang
aarch64-linux-android23-clang aarch64-linux-android32-clang
aarch64-linux-android24-clang aarch64-linux-android33-clang
aarch64-linux-android25-clang aarch64-linux-android34-clang
aarch64-linux-android26-clang armv7a-linux-androideabi21-clang
aarch64-linux-android27-clang armv7a-linux-androideabi22-clang
aarch64-linux-android28-clang armv7a-linux-androideabi23-clang
aarch64-linux-android29-clang armv7a-linux-androideabi24-clang
```
the example output is not exhaustive and is cut short for brevity and example, do take a look
at your downloaded NDK archive for what executables are available to you
many executables are present, so choose a) what architecture you will build for (more often
than not it's `aarch64`), and b) what target android api are you building for
if you're building for yourself, pick an api level/version that correlates to your devices'
android version. as an example, my device runs on an ARM processor (`aarch64`) and runs Android 14,
which is api level 34. (`android34`) as such, i would use the `aarch64-linux-android34-clang`
binary
2. clone the repository at either `https://forge.joshwel.co/mark/surplus` or
`https://github.com/markjoshwel/surplus`, and navigate to `src/spow-whatsapp-bridge` within the
cloned repository
```text
git clone https://forge.joshwel.co/mark/surplus
cd surplus/src/spow-whatsapp-bridge
```
3. build the bridge:
```text
CC="<path to android ndk clang executable>" GOOS=android GOARCH=arm64 CGO_ENABLED=1 go build
```
for compatibility with the documentations' instructions as-is, rename the built binary to
`s+ow-whatsapp-bridge`
```text
mv spow-whatsapp-bridge s+ow-whatsapp-bridge
```
4. send the built binary over to your Termux environment, and then move it into the
`$HOME/.local/bin/` folder. if it doesn't exist, make it with `mkdir` and ensure that the folder
is in your `PATH` variable either using your `.profile`, `.bashrc` or whatever file is sourced
when opening your shell
### post-installation setup
1. log into WhatsApp:
```text
s+ow-whatsapp-bridge login
```
give it a minute or two to sync your history. once the screen stops scrolling, you can safely
exit with Ctrl+D or Ctrl+C.
2. find out what chats you want the bridge to target:
```text
s+ow-whatsapp-bridge list
```
!!! note
for sending to individuals: their IDs are their internationalised phone numbers ending in
`@s.whatsapp.net`
example: `+65 9123 4567` is `6591234567@s.whatsapp.net`
then, note these down, prefixed with `wa:`, to them to your `SPOW_TARGETS` variable in your
s+ow cron job
3. finally, add the following to your $HOME/.s+ow-bridges file:
```text
s+ow-whatsapp-bridge
```
## updating
to keep updated as [whatsmeow](https://github.com/tulir/whatsmeow/), the library the bridge depends
on, has to keep updated with the WhatsApp web multidevice API, you can either:
1. [rebuild when a weekly release comes out](#building-from-source),
2. [or rely on the weekly continuous deployment builds](#from-a-pre-built-binary)
to use the weekly builds without building from scratch every time,
!!! note
this will pull the latest binary, around 20 megabytes in size, every day. if your network or
data plan may not take kindly to this, feel free to adjust the cron entry as you wish, or to
one that runs once a week instead:
```text
0 0 * * 0 bash -l -c "s+ow-whatsapp-bridge-update"
```
## usage
- `s+ow-whatsapp-bridge`
normal usage; sends latest message to wa:-prefixed targets given in stdin
- `s+ow-whatsapp-bridge login`
logs in to WhatsApp
- `s+ow-whatsapp-bridge pair-phone`
logs in to WhatsApp using a phone number
- `s+ow-whatsapp-bridge reconnect`
reconnects the client
- `s+ow-whatsapp-bridge logout`
logs out of WhatsApp
- `s+ow-whatsapp-bridge list`
lists all group chats and their IDs.
for sending to individuals: their IDs are their internationalised phone numbers ending in
`@s.whatsapp.net`
example: `+65 9123 4567` is `6591234567@s.whatsapp.net`
## verifying a pre-built binary
!!! note
if you installed the bridge through an installation script, it would have already
and if the script or `s+ow-whatsapp-bridge-update` throws an error about failing verification,
you can use the environment variable ``
TODO
## versioning scheme
from `v2.2024.25`, the bridge is now versioned with a modified calendar versioning scheme of
`MAJOR.YEAR.ISOWEEK`, where the `MAJOR` version segment will be bumped with codebase changes, whereas
the `YEAR` and `ISOWEEK` segments will represent the time of which the release was built at
## licence
the s+ow Telegram Bridge is free and unencumbered software released into the public domain.
for more information, see [licences](../licences.md).

212
docs/stylesheets/extra.css Normal file
View file

@ -0,0 +1,212 @@
@font-face {
font-family: "Geist";
src: url('../fonts/GeistVF.woff2') format('woff2'),
url('../fonts/Geist-Regular.ttf') format('truetype');
}
@font-face {
font-family: "Geist Mono";
src: url('../fonts/GeistMonoVF.woff2') format('woff2'),
url('../fonts/GeistMono-Regular.ttf') format('truetype');
}
:root {
--md-text-font: "Geist";
--md-code-font: "Geist Mono";
--md-hue: 180deg;
}
* {
text-rendering: geometricprecision !important;
-webkit-font-smoothing: antialiased;
}
[data-md-color-scheme="default"] {
color-scheme: light;
--md-sys-color-primary: rgb(51 71 65);
--md-sys-color-surface-tint: rgb(78 99 92);
--md-sys-color-on-primary: rgb(255 255 255);
--md-sys-color-primary-container: rgb(95 116 109);
--md-sys-color-on-primary-container: rgb(255 255 255);
--md-sys-color-secondary: rgb(61 69 66);
--md-sys-color-on-secondary: rgb(255 255 255);
--md-sys-color-secondary-container: rgb(110 118 115);
--md-sys-color-on-secondary-container: rgb(255 255 255);
--md-sys-color-tertiary: rgb(0 73 93);
--md-sys-color-on-tertiary: rgb(255 255 255);
--md-sys-color-tertiary-container: rgb(65 124 147);
--md-sys-color-on-tertiary-container: rgb(255 255 255);
--md-sys-color-error: rgb(124 37 0);
--md-sys-color-on-error: rgb(255 255 255);
--md-sys-color-error-container: rgb(200 77 28);
--md-sys-color-on-error-container: rgb(255 255 255);
--md-sys-color-background: rgb(251 249 247);
--md-sys-color-on-background: rgb(27 28 27);
--md-sys-color-surface: rgb(251 249 247);
--md-sys-color-on-surface: rgb(27 28 27);
--md-sys-color-surface-variant: rgb(222 228 224);
--md-sys-color-on-surface-variant: rgb(62 68 66);
--md-sys-color-outline: rgb(90 96 94);
--md-sys-color-outline-variant: rgb(118 124 121);
--md-sys-color-shadow: rgb(0 0 0);
--md-sys-color-scrim: rgb(0 0 0);
--md-sys-color-inverse-surface: rgb(48 49 48);
--md-sys-color-inverse-on-surface: rgb(242 240 239);
--md-sys-color-inverse-primary: rgb(181 203 195);
--md-sys-color-primary-fixed: rgb(100 121 114);
--md-sys-color-on-primary-fixed: rgb(255 255 255);
--md-sys-color-primary-fixed-dim: rgb(76 96 90);
--md-sys-color-on-primary-fixed-variant: rgb(255 255 255);
--md-sys-color-secondary-fixed: rgb(110 118 115);
--md-sys-color-on-secondary-fixed: rgb(255 255 255);
--md-sys-color-secondary-fixed-dim: rgb(86 94 90);
--md-sys-color-on-secondary-fixed-variant: rgb(255 255 255);
--md-sys-color-tertiary-fixed: rgb(65 124 147);
--md-sys-color-on-tertiary-fixed: rgb(255 255 255);
--md-sys-color-tertiary-fixed-dim: rgb(36 99 121);
--md-sys-color-on-tertiary-fixed-variant: rgb(255 255 255);
--md-sys-color-surface-dim: rgb(219 218 216);
--md-sys-color-surface-bright: rgb(251 249 247);
--md-sys-color-surface-container-lowest: rgb(255 255 255);
--md-sys-color-surface-container-low: rgb(245 243 242);
--md-sys-color-surface-container: rgb(239 238 236);
--md-sys-color-surface-container-high: rgb(233 232 230);
--md-sys-color-surface-container-highest: rgb(228 226 225);
--md-hue: 139.2deg;
--md-default-fg-color: var(--md-sys-color-primary);
--md-default-bg-color: var(--md-sys-color-surface);
/* primary colours */
--md-primary-fg-color: var(--md-sys-color-primary);
--md-primary-fg-color--light: var(--md-sys-color-inverse-primary);
--md-primary-fg-color--dark: var(--md-sys-color-primary-container);
--md-primary-bg-color: var(--md-sys-color-surface);
--md-primary-bg-color--light: var(--md-sys-color-surface-dim);
/* accent (interactable) colours */
--md-accent-fg-color: var(--md-sys-color-tertiary);
--md-accent-bg-color: var(--md-sys-color-on-tertiary);
--md-accent-bg-color--light: var(--md-sys-color-surface-dim);
/* typesetting colours */
--md-typeset-color: var(--md-sys-color-on-surface);
--md-typeset-a-color: var(--md-sys-color-tertiary);
--md-typeset-del-color: var(--md-sys-color-on-error-container);
--md-typeset-ins-color: var(--md-sys-color-on-primary-container);
--md-typeset-kbd-color: var(--md-sys-color-surface-container-lowest);
--md-typeset-kbd-accent-color: var(--md-sys-color-surface-container);
--md-typeset-kbd-border-color: var(--md-sys-color-surface-container-highest);
--md-typeset-mark-color: var(--md-sys-color-tertiary-container);
--md-typeset-table-color: var(--md-sys-color-outline);
--md-code-bg-color: var(--md-sys-color-surface-container-high);
/* admonition colours */
--md-admonition-fg-color: var(--md-sys-color-secondary);
--md-admonition-bg-color: var(--md-default-bg-color);
--md-warning-fg-color: var(--md-sys-color-on-error-container);
--md-warning-bg-color: var(--md-sys-color-error-container);
/* footer colours */
--md-footer-fg-color: var(--md-sys-color-on-surface);
--md-footer-fg-color--light: var(--md-sys-color-on-surface-variant);
--md-footer-fg-color--lighter: var(--md-sys-color-outline);
--md-footer-bg-color: var(--md-sys-color-surface-dim);
--md-footer-bg-color--dark: var(--md-sys-color-surface-container-highest);
}
[data-md-color-scheme="slate"] {
color-scheme: dark;
--md-sys-color-primary: rgb(185 208 199);
--md-sys-color-surface-tint: rgb(181 203 195);
--md-sys-color-on-primary: rgb(6 26 21);
--md-sys-color-primary-container: rgb(128 149 142);
--md-sys-color-on-primary-container: rgb(0 0 0);
--md-sys-color-secondary: rgb(196 205 200);
--md-sys-color-on-secondary: rgb(16 24 21);
--md-sys-color-secondary-container: rgb(138 147 143);
--md-sys-color-on-secondary-container: rgb(0 0 0);
--md-sys-color-tertiary: rgb(153 211 236);
--md-sys-color-on-tertiary: rgb(0 25 34);
--md-sys-color-tertiary-container: rgb(99 157 181);
--md-sys-color-on-tertiary-container: rgb(0 0 0);
--md-sys-color-error: rgb(255 187 164);
--md-sys-color-on-error: rgb(48 9 0);
--md-sys-color-error-container: rgb(237 104 54);
--md-sys-color-on-error-container: rgb(0 0 0);
--md-sys-color-background: rgb(19 20 19);
--md-sys-color-on-background: rgb(228 226 225);
--md-sys-color-surface: rgb(19 20 19);
--md-sys-color-on-surface: rgb(252 250 249);
--md-sys-color-surface-variant: rgb(66 72 70);
--md-sys-color-on-surface-variant: rgb(198 204 200);
--md-sys-color-outline: rgb(158 164 161);
--md-sys-color-outline-variant: rgb(126 132 129);
--md-sys-color-shadow: rgb(0 0 0);
--md-sys-color-scrim: rgb(0 0 0);
--md-sys-color-inverse-surface: rgb(228 226 225);
--md-sys-color-inverse-on-surface: rgb(41 42 41);
--md-sys-color-inverse-primary: rgb(56 76 70);
--md-sys-color-primary-fixed: rgb(209 232 223);
--md-sys-color-on-primary-fixed: rgb(2 20 16);
--md-sys-color-primary-fixed-dim: rgb(181 203 195);
--md-sys-color-on-primary-fixed-variant: rgb(38 58 52);
--md-sys-color-secondary-fixed: rgb(220 228 224);
--md-sys-color-on-secondary-fixed: rgb(11 19 16);
--md-sys-color-secondary-fixed-dim: rgb(192 200 196);
--md-sys-color-on-secondary-fixed-variant: rgb(48 56 53);
--md-sys-color-tertiary-fixed: rgb(186 234 255);
--md-sys-color-on-tertiary-fixed: rgb(0 20 27);
--md-sys-color-tertiary-fixed-dim: rgb(149 207 232);
--md-sys-color-on-tertiary-fixed-variant: rgb(0 59 76);
--md-sys-color-surface-dim: rgb(19 20 19);
--md-sys-color-surface-bright: rgb(57 57 56);
--md-sys-color-surface-container-lowest: rgb(13 14 14);
--md-sys-color-surface-container-low: rgb(27 28 27);
--md-sys-color-surface-container: rgb(31 32 31);
--md-sys-color-surface-container-high: rgb(41 42 41);
--md-sys-color-surface-container-highest: rgb(52 53 52);
/*--md-hue: 139.2deg;*/
--md-default-fg-color: var(--md-sys-color-primary);
--md-default-bg-color: var(--md-sys-color-surface);
/* primary colours */
--md-primary-fg-color: var(--md-sys-color-primary);
--md-primary-fg-color--light: var(--md-sys-color-inverse-primary);
--md-primary-fg-color--dark: var(--md-sys-color-primary-container);
--md-primary-bg-color: var(--md-sys-color-surface);
--md-primary-bg-color--light: var(--md-sys-color-surface-dim);
/* accent (interactable) colours */
--md-accent-fg-color: var(--md-sys-color-tertiary);
--md-accent-bg-color: var(--md-sys-color-on-tertiary);
--md-accent-bg-color--light: var(--md-sys-color-surface-dim);
/* typesetting colours */
--md-typeset-color: var(--md-sys-color-on-surface);
--md-typeset-a-color: var(--md-sys-color-tertiary);
--md-typeset-del-color: var(--md-sys-color-on-error-container);
--md-typeset-ins-color: var(--md-sys-color-on-primary-container);
--md-typeset-kbd-color: var(--md-sys-color-surface-container-lowest);
--md-typeset-kbd-accent-color: var(--md-sys-color-surface-container);
--md-typeset-kbd-border-color: var(--md-sys-color-surface-container-highest);
--md-typeset-mark-color: var(--md-sys-color-tertiary-container);
--md-typeset-table-color: var(--md-sys-color-outline);
--md-typeset-table-color--light: var(--md-sys-color-outline-variant);
--md-code-bg-color: var(--md-sys-color-surface-container-high);
/* admonition colours */
--md-admonition-fg-color: var(--md-sys-color-secondary);
--md-admonition-bg-color: var(--md-default-bg-color);
--md-warning-fg-color: var(--md-sys-color-on-error-container);
--md-warning-bg-color: var(--md-sys-color-error-container);
/* footer colours */
--md-footer-fg-color: var(--md-sys-color-on-surface);
--md-footer-fg-color--light: var(--md-sys-color-on-surface-variant);
--md-footer-fg-color--lighter: var(--md-sys-color-outline);
--md-footer-bg-color: var(--md-sys-color-surface-dim);
--md-footer-bg-color--dark: var(--md-sys-color-surface-container-highest);
}

View file

@ -0,0 +1,4 @@
@page {
size: A4;
margin: 1.25cm;
}

11
docs/using.md Normal file
View file

@ -0,0 +1,11 @@
# the user's handbook
TODO
## as a command line tool
TODO
## as a python library
TODO

78
flake.lock Normal file
View file

@ -0,0 +1,78 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1718318537,
"narHash": "sha256-4Zu0RYRcAY/VWuu6awwq4opuiD//ahpc2aFHg2CWqFY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9ee548d90ff586a6471b4ae80ae9cfcbceb3420",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-old-hatch": {
"locked": {
"lastModified": 1702508050,
"narHash": "sha256-imn+/Rj+bqagOSm7GmRDbrqkxtc7QnjY3Cu85gv46BU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fd04bea4cbf76f86f244b9e2549fca066db8ddff",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "fd04bea4cbf76f86f244b9e2549fca066db8ddff",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"nixpkgs-old-hatch": "nixpkgs-old-hatch"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

45
flake.nix Normal file
View file

@ -0,0 +1,45 @@
{
description = "development environment for surplus";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# https://github.com/NixOS/nixpkgs/issues/308121
nixpkgs-old-hatch.url = "github:NixOS/nixpkgs/fd04bea4cbf76f86f244b9e2549fca066db8ddff";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, nixpkgs-old-hatch, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
inherit (nixpkgs) lib;
pkgs = nixpkgs.legacyPackages.${system};
pkgs-old-hatch = nixpkgs-old-hatch.legacyPackages.${system};
in
{
devShells.default = pkgs.mkShellNoCC {
NIX_LD_LIBRARY_PATH = lib.makeLibraryPath [
pkgs.stdenv.cc.cc
];
NIX_LD = lib.fileContents "${pkgs.stdenv.cc}/nix-support/dynamic-linker";
buildInputs =
[
# surplus
pkgs.python3
pkgs-old-hatch.hatch
# mkdocs-exporter
pkgs.stdenv.cc.cc.lib
pkgs.playwright
];
shellHook = ''
export LD_LIBRARY_PATH=$NIX_LD_LIBRARY_PATH
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
echo LD_LIBRARY_PATH=$LD_LIBRARY_PATH
echo PLAYWRIGHT_BROWSERS_PATH=$PLAYWRIGHT_BROWSERS_PATH
echo PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=$PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS
'';
};
}
);
}

114
mkdocs.yml Normal file
View file

@ -0,0 +1,114 @@
site_name: surplus Documentation
site_url: https://surplus.joshwel.co
site_author: Mark Joshwel and surplus contributors
site_description: documentation for the surplus and sibling projects
repo_name: markjoshwel/surplus
repo_url: https://github.com/markjoshwel/surplus
copyright: |
with with all our hearts, 2023-2024, mark joshwel and contributors<br>
documentation is dedicated to the public domain with <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0</a>
nav:
- about:
- surplus: "index.md"
- licences: "licences.md"
- changelog: "changelog.md"
- handbooks:
- "using.md"
- "developing.md"
- "contributing.md"
- on wheels:
- "onwheels/index.md"
- bridges:
- about bridges: "onwheels/bridges.md"
- "onwheels/telegram-bridge.md"
- "onwheels/whatsapp-bridge.md"
- "onwheels/emulating-termux-location.md"
- "onwheels/emulating-termux-notification.md"
- backup links:
"links.md"
theme:
name: material
language: en
features:
- navigation.tabs
- navigation.tabs.sticky
- navigation.tracking
- navigation.expand
- toc.integrate
- search.suggest
- search.highlight
- content.tabs.link
- content.code.annotation
- content.code.copy
- pymdownx.snippets
font: false
palette:
- media: "(prefers-color-scheme)"
toggle:
icon: material/brightness-auto
name: Light Theme
primary: custom
accent: custom
- media: "(prefers-color-scheme: light)"
scheme: default
toggle:
icon: material/brightness-7
name: Dark Theme
primary: custom
accent: custom
- media: "(prefers-color-scheme: dark)"
scheme: slate
toggle:
icon: material/brightness-4
name: System Theme
primary: custom
accent: custom
icon:
admonition:
abstract: material/text-box-outline
tip: material/pencil-outline
note: material/information-slab-box-outline
warning: material/alert-outline
danger: material/alert-octagon-outline
extra_css:
- stylesheets/extra.css
plugins:
- search
- privacy
#- git-revision-date-localized:
# enable_creation_date: true
- exporter:
formats:
pdf:
enabled: !ENV [MKDOCS_EXPORTER_PDF_ENABLED, true]
stylesheets:
- docs/stylesheets/pdf.scss
aggregator:
enabled: true
output: documentation.pdf
buttons:
- title: Download as PDF
icon: material-file-download-outline
enabled: !!python/name:mkdocs_exporter.formats.pdf.buttons.download.enabled
attributes: !!python/name:mkdocs_exporter.formats.pdf.buttons.download.attributes
markdown_extensions:
- admonition
- pymdownx.highlight:
anchor_linenums: true
line_spans: __span
pygments_lang_class: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences

241
poetry.lock generated
View file

@ -1,241 +0,0 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "black"
version = "24.2.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"},
{file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"},
{file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"},
{file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"},
{file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"},
{file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"},
{file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"},
{file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"},
{file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"},
{file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"},
{file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"},
{file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"},
{file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"},
{file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"},
{file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"},
{file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"},
{file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"},
{file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"},
{file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"},
{file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"},
{file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"},
{file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "geographiclib"
version = "2.0"
description = "The geodesic routines from GeographicLib"
optional = false
python-versions = ">=3.7"
files = [
{file = "geographiclib-2.0-py3-none-any.whl", hash = "sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734"},
{file = "geographiclib-2.0.tar.gz", hash = "sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859"},
]
[[package]]
name = "geopy"
version = "2.4.1"
description = "Python Geocoding Toolbox"
optional = false
python-versions = ">=3.7"
files = [
{file = "geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7"},
{file = "geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1"},
]
[package.dependencies]
geographiclib = ">=1.52,<3"
[package.extras]
aiohttp = ["aiohttp"]
dev = ["coverage", "flake8 (>=5.0,<5.1)", "isort (>=5.10.0,<5.11.0)", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "readme-renderer", "sphinx (<=4.3.2)", "sphinx-issues", "sphinx-rtd-theme (>=0.5.0)"]
dev-docs = ["readme-renderer", "sphinx (<=4.3.2)", "sphinx-issues", "sphinx-rtd-theme (>=0.5.0)"]
dev-lint = ["flake8 (>=5.0,<5.1)", "isort (>=5.10.0,<5.11.0)"]
dev-test = ["coverage", "pytest (>=3.10)", "pytest-asyncio (>=0.17)", "sphinx (<=4.3.2)"]
requests = ["requests (>=2.16.2)", "urllib3 (>=1.24.2)"]
timezone = ["pytz"]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "mypy"
version = "1.8.0"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:485a8942f671120f76afffff70f259e1cd0f0cfe08f81c05d8816d958d4577d3"},
{file = "mypy-1.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:df9824ac11deaf007443e7ed2a4a26bebff98d2bc43c6da21b2b64185da011c4"},
{file = "mypy-1.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2afecd6354bbfb6e0160f4e4ad9ba6e4e003b767dd80d85516e71f2e955ab50d"},
{file = "mypy-1.8.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8963b83d53ee733a6e4196954502b33567ad07dfd74851f32be18eb932fb1cb9"},
{file = "mypy-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:e46f44b54ebddbeedbd3d5b289a893219065ef805d95094d16a0af6630f5d410"},
{file = "mypy-1.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:855fe27b80375e5c5878492f0729540db47b186509c98dae341254c8f45f42ae"},
{file = "mypy-1.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4c886c6cce2d070bd7df4ec4a05a13ee20c0aa60cb587e8d1265b6c03cf91da3"},
{file = "mypy-1.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d19c413b3c07cbecf1f991e2221746b0d2a9410b59cb3f4fb9557f0365a1a817"},
{file = "mypy-1.8.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9261ed810972061388918c83c3f5cd46079d875026ba97380f3e3978a72f503d"},
{file = "mypy-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:51720c776d148bad2372ca21ca29256ed483aa9a4cdefefcef49006dff2a6835"},
{file = "mypy-1.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:52825b01f5c4c1c4eb0db253ec09c7aa17e1a7304d247c48b6f3599ef40db8bd"},
{file = "mypy-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f5ac9a4eeb1ec0f1ccdc6f326bcdb464de5f80eb07fb38b5ddd7b0de6bc61e55"},
{file = "mypy-1.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afe3fe972c645b4632c563d3f3eff1cdca2fa058f730df2b93a35e3b0c538218"},
{file = "mypy-1.8.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:42c6680d256ab35637ef88891c6bd02514ccb7e1122133ac96055ff458f93fc3"},
{file = "mypy-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:720a5ca70e136b675af3af63db533c1c8c9181314d207568bbe79051f122669e"},
{file = "mypy-1.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:028cf9f2cae89e202d7b6593cd98db6759379f17a319b5faf4f9978d7084cdc6"},
{file = "mypy-1.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4e6d97288757e1ddba10dd9549ac27982e3e74a49d8d0179fc14d4365c7add66"},
{file = "mypy-1.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f1478736fcebb90f97e40aff11a5f253af890c845ee0c850fe80aa060a267c6"},
{file = "mypy-1.8.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:42419861b43e6962a649068a61f4a4839205a3ef525b858377a960b9e2de6e0d"},
{file = "mypy-1.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:2b5b6c721bd4aabaadead3a5e6fa85c11c6c795e0c81a7215776ef8afc66de02"},
{file = "mypy-1.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c1538c38584029352878a0466f03a8ee7547d7bd9f641f57a0f3017a7c905b8"},
{file = "mypy-1.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4ef4be7baf08a203170f29e89d79064463b7fc7a0908b9d0d5114e8009c3a259"},
{file = "mypy-1.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7178def594014aa6c35a8ff411cf37d682f428b3b5617ca79029d8ae72f5402b"},
{file = "mypy-1.8.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ab3c84fa13c04aeeeabb2a7f67a25ef5d77ac9d6486ff33ded762ef353aa5592"},
{file = "mypy-1.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:99b00bc72855812a60d253420d8a2eae839b0afa4938f09f4d2aa9bb4654263a"},
{file = "mypy-1.8.0-py3-none-any.whl", hash = "sha256:538fd81bb5e430cc1381a443971c0475582ff9f434c16cd46d2c66763ce85d9d"},
{file = "mypy-1.8.0.tar.gz", hash = "sha256:6ff8b244d7085a0b425b56d327b480c3b29cafbd2eff27316a004f9a7391ae07"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
typing-extensions = ">=4.1.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.2.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
{file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
[[package]]
name = "pluscodes"
version = "2022.1.3"
description = "Compute Plus Codes (Open Location Codes)."
optional = false
python-versions = ">=3.10"
files = [
{file = "pluscodes-2022.1.3-py3-none-any.whl", hash = "sha256:50625f472f8d4e8822e005180c2eb41bf09e45e429f362d3cded346f1169dae8"},
]
[package.extras]
dev = ["black (==22.3.0)", "build (==0.8.0)", "coverage (==6.4)", "isort (==5.8.0)", "pylintv (==2.15.0)", "pytest (==7.1.1)", "pytest-cov (==3.0.0)", "twine (==4.0.1)"]
[[package]]
name = "typing-extensions"
version = "4.9.0"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"},
{file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "7e98867e42e78af873d571b1f5e1ac66b21f4147b88ea629eb9f4d808c8f84c5"

View file

@ -1,37 +1,97 @@
[tool.poetry] [build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "surplus" name = "surplus"
version = "2.2.0" dynamic = ["version"]
description = "Python script to convert Google Maps Plus Codes to iOS Shortcuts-like shareable text." description = 'convert Plus Codes, coordinates or location strings to shareable text'
authors = ["Mark Joshwel <mark@joshwel.co>"]
license = "Unlicense"
readme = "README.md" readme = "README.md"
repository = "https://github.com/markjoshwel/surplus" requires-python = ">=3.11"
license = "Unlicense"
keywords = ["pluscodes", "openlocationcode"] keywords = ["pluscodes", "openlocationcode"]
packages = [ authors = [
{include = "surplus"} { name = "Mark Joshwel", email = "mark@joshwel.co" },
]
classifiers = [
"Development Status :: 6 - Mature",
"Programming Language :: Python",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dependencies = [
"pluscodes~=2022.1.3",
"geopy~=2.4.1",
] ]
[tool.poetry.dependencies] [project.scripts]
python = "^3.11" surplus = "surplus:cli"
pluscodes = "^2022.1.3" "s+" = "surplus:cli"
geopy = "^2.4.1"
[tool.poetry.group.dev.dependencies] [tool.hatch.build.targets.sdist]
black = "^24.2.0" exclude = [
mypy = "^1.8.0" "/.github",
isort = "^5.12.0" "/.devbox",
"/src/surplus-on-wheels",
"/src/spow*",
]
[tool.poetry.scripts] [tool.hatch.build.targets.wheel]
surplus = 'surplus:cli' packages = ["src.surplus"]
"s+" = 'surplus:cli'
[tool.black] [project.urls]
line-length = 90 Documentation = "https://surplus.joshwel.co"
Issues = "https://surplus.joshwel.co/issues"
Source = "https://github.com/markjoshwel/surplus"
Changelog = "https://surplus.joshwel.co/changelog"
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.isort] [tool.isort]
line_length = 90 line_length = 100
profile = "black" profile = "black"
[build-system] [tool.hatch.version]
requires = ["poetry-core"] path = "src/surplus/surplus.py"
build-backend = "poetry.core.masonry.api"
[[tool.hatch.envs.all.matrix]]
python = ["3.11", "3.12"]
[tool.hatch.envs.default]
description = "default development environment"
dependencies = ["mypy", "ruff", "isort"]
[tool.hatch.envs.default.scripts]
check = [
"mypy src",
"hatch fmt --check",
"isort --check src"
]
format = [
"hatch fmt -f",
"isort src"
]
[tool.hatch.envs.hatch-static-analysis]
dependencies = ["ruff>=0.3.2"]
[tool.hatch.envs.docs]
detached = true
description = "env for generator documentation"
dependencies = [
"mkdocs",
"mkdocs-material",
"mkdocs-git-revision-date-localized-plugin",
"mkdocs-exporter",
"playwright",
]
[tool.hatch.envs.docs.scripts]
build = [
"python src/tools/docs-prebuild.py",
"mkdocs build --clean --strict",
]
serve = "mkdocs serve --dev-addr localhost:8000"

View file

@ -1,8 +0,0 @@
geographiclib==2.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:6b7225248e45ff7edcee32becc4e0a1504c606ac5ee163a5656d482e0cd38734 \
--hash=sha256:f7f41c85dc3e1c2d3d935ec86660dc3b2c848c83e17f9a9e51ba9d5146a15859
geopy==2.4.0 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:a59392bf17adb486b25dbdd71fbed27733bdf24a2dac588047a619de56695e36 \
--hash=sha256:d2639a46d0ce4c091e9688b750ba94348a14b898a1e55c68f4b4a07e7d1afa20
pluscodes==2022.1.3 ; python_version >= "3.11" and python_version < "4.0" \
--hash=sha256:50625f472f8d4e8822e005180c2eb41bf09e45e429f362d3cded346f1169dae8

51
src/spow-telegram-bridge/.gitignore vendored Normal file
View file

@ -0,0 +1,51 @@
.helper
*.session
*.session-journal
# cached files
__pycache__/
*.py[cod]
*$py.class
# distribution
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
*.so
MANIFEST
pip-log.txt
pip-delete-this-directory.txt
# venv
.python-version
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# nix
.devbox
result

View file

@ -0,0 +1,5 @@
# surplus on wheels: Telegram Bridge
see <https://surplus.joshwel.co/onwheels/telegram-bridge>
or [/docs/onwheels/telegram-bridge.md](../../docs/onwheels/telegram-bridge.md)
for more information and documentation

View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View file

@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""
s+ow-telegram-bridge: add-on bridge for surplus on wheels (s+ow) to telegram
----------------------------------------------------------------------------
by mark <mark@joshwel.co>
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
"""
import asyncio
from os import environ
from pathlib import Path
from sys import argv, stderr, stdin
from traceback import print_tb
from typing import Final
from telethon import TelegramClient # type: ignore
# exit codes:
# 1 - bad command usage or missing env vars
# 2 - bad target
# 3 - could not send message
# rundown:
# 1. if argv[-1] is 'login', then run login() and exit
# 2. read stdin and comma split it
# 3. read ~/.cache/s+ow/message
# 4. for each target in comma split stdin that starts with "tg:",
# send ~/.cache/s+ow/message
SESSION_NAME: Final[str] = "spowtg"
dir_data: Path = Path.home().joinpath(".local/share/s+ow-telegram-bridge")
dir_data.mkdir(parents=True, exist_ok=True)
dir_cache: Path = Path.home().joinpath(".cache/s+ow-telegram-bridge")
dir_cache.mkdir(parents=True, exist_ok=True)
api_id: str | None = environ.get("SPOW_TELEGRAM_API_ID", None)
api_hash: str | None = environ.get("SPOW_TELEGRAM_API_HASH", None)
message_file: Path = Path.home().joinpath(".cache/s+ow/message")
session_file: Path = dir_data.joinpath(f"{SESSION_NAME}.session")
def handle_error(
exc: Exception | None = None,
err_message: str = "error",
recoverable: bool = False,
exit_code: int = -1,
) -> None:
try:
exc_details: str = ""
if isinstance(exc, Exception):
exc_details = f": {exc} ({exc.__class__.__name__})"
print_tb(exc.__traceback__, file=stderr)
print(
f"s+ow-telegram-bridge: {err_message}{exc_details}",
file=stderr,
)
except Exception:
pass
if not recoverable:
exit(exit_code)
def validate_vars() -> None:
if api_id is None:
print("s+ow-telegram-bridge: error: SPOW_TELEGRAM_API_ID not set", file=stderr)
exit(1)
if api_hash is None:
print("s+ow-telegram-bridge: error: SPOW_TELEGRAM_API_HASH not set", file=stderr)
exit(1)
async def run() -> None:
silent: bool = "--silent" in argv
delete_last: bool = "--delete-last" in argv
if silent:
print("s+ow-telegram-bridge: info: --silent passed", file=stderr)
if delete_last:
print("s+ow-telegram-bridge: info: --delete-last passed", file=stderr)
targets: list[int] = []
# "spec" point 2:
# reads in SPOW_TARGETS given by surplus to the bridge using stdin
# "spec" point 2(a):
# bridges do not need to account for the possibility of multiple lines sent to stdin
# this bridge doesn't do this because it's simpler to iterate through stdin with a 'for'
# loop in python
for line in stdin:
for _target in line.split(","):
# "spec" point 2(b):
# bridges should account for the possibility of comma and space delimited targets
_target = _target.strip()
# "spec" point 2(c):
# bridges should recognise a platform based on a prefix
if _target.startswith("tg:"):
_target = _target[3:]
if not (
_target.isnumeric()
or (_target.startswith("-") and _target.lstrip("-").isnumeric())
):
continue
try:
targets.append(int(_target))
except Exception as exc:
handle_error(
exc=exc,
err_message=f"error: could not cast '{_target}' as int",
recoverable=True,
exit_code=2,
)
continue
# "spec" point 3:
# reads SPOW_MESSAGE (~/.cache/spow/message) for the message content
if not (message_file.exists() and message_file.is_file()):
print("s+ow-telegram-bridge: error: ~/.cache/s+ow/message not found", file=stderr)
exit(1)
message = message_file.read_text(encoding="utf-8")
async with TelegramClient(session_file, api_id, api_hash) as client:
for target in targets:
try:
if delete_last is False:
await client.send_message(
int(target),
message,
silent=silent,
)
else:
target_persist: Path = dir_cache.joinpath(str(target))
try:
# delete old message if persist file exists
if target_persist.exists() and target_persist.is_file():
await client.delete_messages(
entity=target,
message_ids=[int(target_persist.read_text(encoding="utf-8"))],
)
except Exception as exc:
handle_error(
exc=exc,
err_message="error: could not delete old message",
recoverable=True,
exit_code=3,
)
continue
# send new message
target_sent_message = await client.send_message(
target,
message,
silent=silent,
)
# persist new message id
target_persist.write_text(str(target_sent_message.id), encoding="utf-8")
except Exception as exc:
handle_error(
exc=exc,
err_message="error: could not send message",
recoverable=True,
exit_code=3,
)
continue
print("s+ow-telegram-bridge: success: message sent to", target)
exit()
def login() -> None:
with TelegramClient(session_file, api_id, api_hash) as client:
client.start()
exit()
def logout() -> None:
if session_file.exists():
session_file.unlink()
print("s+ow-telegram-bridge: logged out successfully", file=stderr)
else:
print("s+ow-telegram-bridge: already logged out", file=stderr)
def list_chats() -> None:
with TelegramClient(session_file, api_id, api_hash) as client:
for dialog in client.iter_dialogs():
print(dialog.id, "\t", dialog.name)
exit()
def entry() -> None:
if len(argv) < 1:
print("s+ow-telegram-bridge: error: len(argv) < 1", file=stderr)
exit(1)
if "login" in argv:
validate_vars()
login()
elif "logout" in argv:
logout()
elif "list" in argv:
validate_vars()
list_chats()
else:
asyncio.run(run())
if __name__ == "__main__":
entry()

View file

@ -0,0 +1,21 @@
#!/bin/sh
failures=0
mypy bridge.py
failures=$((failures + $?))
ruff check bridge.py
failures=$((failures + $?))
ruff format bridge.py
failures=$((failures + $?))
isort --check bridge.py
failures=$((failures + $?))
if [ $failures -eq 0 ]; then
printf "\n\nall checks okay! (❁´◡\`❁)\n"
else
printf "\n\nsome checks failed...\n"
fi
exit $failures

View file

@ -0,0 +1,175 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-github-actions": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1703863825,
"narHash": "sha256-rXwqjtwiGKJheXB43ybM8NwWB8rO2dSRrEqes0S7F5Y=",
"owner": "nix-community",
"repo": "nix-github-actions",
"rev": "5163432afc817cf8bd1f031418d1869e4c9d5547",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-github-actions",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1718530797,
"narHash": "sha256-pup6cYwtgvzDpvpSCFh1TEUjw2zkNpk8iolbKnyFmmU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "b60ebf54c15553b393d144357375ea956f89e9a9",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"poetry2nix": {
"inputs": {
"flake-utils": "flake-utils_2",
"nix-github-actions": "nix-github-actions",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems_3",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1718656656,
"narHash": "sha256-/8pXTFOfb7+KrFi+g8G/dFehDkc96/O5eL8L+FjzG1w=",
"owner": "nix-community",
"repo": "poetry2nix",
"rev": "2c6d07717af20e45fa5b2c823729126be91a3cdf",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "poetry2nix",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"poetry2nix": "poetry2nix"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"id": "systems",
"type": "indirect"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"poetry2nix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1718522839,
"narHash": "sha256-ULzoKzEaBOiLRtjeY3YoGFJMwWSKRYOic6VNw2UyTls=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "68eb1dc333ce82d0ab0c0357363ea17c31ea1f81",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -0,0 +1,35 @@
{
description = "development environment for surplus on wheels: Telegram Bridge";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
poetry2nix = {
url = "github:nix-community/poetry2nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { self, nixpkgs, flake-utils, poetry2nix }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
inherit (poetry2nix.lib.mkPoetry2Nix { inherit pkgs; }) mkPoetryEnv;
poetryEnv = mkPoetryEnv {
python = pkgs.python311;
projectDir = self;
preferWheels = true;
};
in
{
# nix develop
devShells.default = pkgs.mkShellNoCC {
packages = [
pkgs.poetry
poetryEnv
];
};
}
);
}

View file

@ -0,0 +1,5 @@
#!/bin/sh
# surplus on wheels: Telegram Bridge: installation and updater script
set -e
echo TODO

167
src/spow-telegram-bridge/poetry.lock generated Normal file
View file

@ -0,0 +1,167 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "mypy"
version = "1.10.1"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"},
{file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"},
{file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"},
{file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"},
{file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"},
{file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"},
{file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"},
{file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"},
{file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"},
{file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"},
{file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"},
{file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"},
{file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"},
{file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"},
{file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"},
{file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"},
{file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"},
{file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"},
{file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"},
{file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"},
{file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"},
{file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"},
{file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"},
{file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"},
{file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"},
{file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"},
{file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"},
]
[package.dependencies]
mypy-extensions = ">=1.0.0"
typing-extensions = ">=4.1.0"
[package.extras]
dmypy = ["psutil (>=4.0)"]
install-types = ["pip"]
mypyc = ["setuptools (>=50)"]
reports = ["lxml"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "pyaes"
version = "1.6.1"
description = "Pure-Python Implementation of the AES block-cipher and common modes of operation"
optional = false
python-versions = "*"
files = [
{file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"},
]
[[package]]
name = "pyasn1"
version = "0.6.0"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
files = [
{file = "pyasn1-0.6.0-py2.py3-none-any.whl", hash = "sha256:cca4bb0f2df5504f02f6f8a775b6e416ff9b0b3b16f7ee80b5a3153d9b804473"},
{file = "pyasn1-0.6.0.tar.gz", hash = "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c"},
]
[[package]]
name = "rsa"
version = "4.9"
description = "Pure-Python RSA implementation"
optional = false
python-versions = ">=3.6,<4"
files = [
{file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"},
{file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"},
]
[package.dependencies]
pyasn1 = ">=0.1.3"
[[package]]
name = "ruff"
version = "0.5.2"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.5.2-py3-none-linux_armv6l.whl", hash = "sha256:7bab8345df60f9368d5f4594bfb8b71157496b44c30ff035d1d01972e764d3be"},
{file = "ruff-0.5.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:1aa7acad382ada0189dbe76095cf0a36cd0036779607c397ffdea16517f535b1"},
{file = "ruff-0.5.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:aec618d5a0cdba5592c60c2dee7d9c865180627f1a4a691257dea14ac1aa264d"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b62adc5ce81780ff04077e88bac0986363e4a3260ad3ef11ae9c14aa0e67ef"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc42ebf56ede83cb080a50eba35a06e636775649a1ffd03dc986533f878702a3"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15c6e9f88c67ffa442681365d11df38afb11059fc44238e71a9d9f1fd51de70"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d3de9a5960f72c335ef00763d861fc5005ef0644cb260ba1b5a115a102157251"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe5a968ae933e8f7627a7b2fc8893336ac2be0eb0aace762d3421f6e8f7b7f83"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a04f54a9018f75615ae52f36ea1c5515e356e5d5e214b22609ddb546baef7132"},
{file = "ruff-0.5.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed02fb52e3741f0738db5f93e10ae0fb5c71eb33a4f2ba87c9a2fa97462a649"},
{file = "ruff-0.5.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3cf8fe659f6362530435d97d738eb413e9f090e7e993f88711b0377fbdc99f60"},
{file = "ruff-0.5.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:237a37e673e9f3cbfff0d2243e797c4862a44c93d2f52a52021c1a1b0899f846"},
{file = "ruff-0.5.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2a2949ce7c1cbd8317432ada80fe32156df825b2fd611688814c8557824ef060"},
{file = "ruff-0.5.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:481af57c8e99da92ad168924fd82220266043c8255942a1cb87958b108ac9335"},
{file = "ruff-0.5.2-py3-none-win32.whl", hash = "sha256:f1aea290c56d913e363066d83d3fc26848814a1fed3d72144ff9c930e8c7c718"},
{file = "ruff-0.5.2-py3-none-win_amd64.whl", hash = "sha256:8532660b72b5d94d2a0a7a27ae7b9b40053662d00357bb2a6864dd7e38819084"},
{file = "ruff-0.5.2-py3-none-win_arm64.whl", hash = "sha256:73439805c5cb68f364d826a5c5c4b6c798ded6b7ebaa4011f01ce6c94e4d5583"},
{file = "ruff-0.5.2.tar.gz", hash = "sha256:2c0df2d2de685433794a14d8d2e240df619b748fbe3367346baa519d8e6f1ca2"},
]
[[package]]
name = "telethon"
version = "1.36.0"
description = "Full-featured Telegram client library for Python 3"
optional = false
python-versions = ">=3.5"
files = [
{file = "Telethon-1.36.0.tar.gz", hash = "sha256:11db5c7ed7e37f1272d443fb7eea0f1db580d56c6949165233946fb323aaf3a7"},
]
[package.dependencies]
pyaes = "*"
rsa = "*"
[package.extras]
cryptg = ["cryptg"]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "613e275515a3ca76e83872c16883548a10a6ad913a6be381a52acf0c4680bd1d"

View file

@ -0,0 +1,40 @@
[tool.poetry]
name = "spow-telegram-bridge"
version = "2.2024.29"
description = "add-on bridge for surplus on wheels (s+ow) to telegram"
authors = ["Mark Joshwel <mark@joshwel.co>"]
license = "Unlicense"
readme = "README.md"
packages = [
{include = "bridge.py"}
]
[tool.poetry.dependencies]
python = "^3.11"
Telethon = "^1.36.0"
[tool.poetry.scripts]
spow-telegram-bridge = 'bridge:entry'
"s+ow-telegram-bridge" = 'bridge:entry'
[tool.poetry.group.dev.dependencies]
# https://github.com/nix-community/poetry2nix/blob/master/docs/edgecases.md#errors-that-are-related-to-rust-and-cargo
# if bumping this, also update the flake.nix file
ruff = "0.5.2"
mypy = "^1.10.1"
isort = "^5.13.2"
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.black]
line-length = 100
[tool.isort]
line_length = 100
profile = "black"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

8
src/spow-whatsapp-bridge/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
spow-whatsapp-bridge
mdtest.db
dist/
test/
# nix
.devbox
result

View file

@ -0,0 +1,374 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View file

@ -0,0 +1,5 @@
# surplus on wheels: WhatsApp Bridge
see <https://surplus.joshwel.co/onwheels/whatsapp-bridge>
or [/docs/onwheels/whatsapp-bridge.md](../../docs/onwheels/whatsapp-bridge.md)
for more information and documentation

View file

@ -0,0 +1,430 @@
// Copyright (c) 2021 Tulir Asokan
// Copyright (c) 2024 Mark Joshwel
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// from https://github.com/tulir/whatsmeow/commit/792d96fbe610bfbf1039ec3b8d3f37f630025aea
package main
import (
"bufio"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"mime"
"os"
"os/signal"
"path"
"strings"
"sync/atomic"
"syscall"
"time"
_ "github.com/mattn/go-sqlite3"
"github.com/mdp/qrterminal/v3"
"google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
waProto "go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
)
var cli *whatsmeow.Client
var log waLog.Logger
var logLevel = "INFO"
var debugLogs = flag.Bool("debug", false, "Enable debug logs?")
var dbDialect = flag.String("db-dialect", "sqlite3", "Database dialect (sqlite3 or postgres)")
var dbAddress = flag.String("db-address", "file:mdtest.db?_foreign_keys=on", "Database address")
var requestFullSync = flag.Bool("request-full-sync", false, "Request full (1 year) history sync when logging in?")
var pairRejectChan = make(chan bool, 1)
// s+ow-whatsapp-bridge: define important paths
var dataDir = path.Join(os.Getenv("HOME"), ".local", "share", "s+ow-whatsapp-bridge")
var sharetextPath = path.Join(os.Getenv("HOME"), ".cache", "s+ow", "message")
func main() {
waBinary.IndentXML = true
flag.Parse()
if *debugLogs {
logLevel = "DEBUG"
}
if *requestFullSync {
store.DeviceProps.RequireFullSync = proto.Bool(true)
store.DeviceProps.HistorySyncConfig = &waProto.DeviceProps_HistorySyncConfig{
FullSyncDaysLimit: proto.Uint32(3650),
FullSyncSizeMbLimit: proto.Uint32(102400),
StorageQuotaMb: proto.Uint32(102400),
}
}
log = waLog.Stdout("Main", logLevel, true)
// s+ow-whatsapp-bridge: make and change dir
err := os.MkdirAll(dataDir, os.ModePerm)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: s+ow-whatsapp-bridge: Failed to create directory: %v", err)
return
}
err = os.Chdir(dataDir)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: s+ow-whatsapp-bridge: Failed to change directory: %v", err)
return
}
dbLog := waLog.Stdout("Database", logLevel, true)
storeContainer, err := sqlstore.New(*dbDialect, *dbAddress, dbLog)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to connect to database: %v", err)
return
}
device, err := storeContainer.GetFirstDevice()
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to get device: %v", err)
return
}
cli = whatsmeow.NewClient(device, waLog.Stdout("Client", logLevel, true))
var isWaitingForPair atomic.Bool
cli.PrePairCallback = func(jid types.JID, platform, businessName string) bool {
isWaitingForPair.Store(true)
defer isWaitingForPair.Store(false)
log.Infof("s+ow-whatsapp-bridge: Pairing %s (platform: %q, business name: %q). Type r within 3 seconds to reject pair", jid, platform, businessName)
select {
case reject := <-pairRejectChan:
if reject {
log.Infof("s+ow-whatsapp-bridge: Rejecting pair")
return false
}
case <-time.After(3 * time.Second):
}
log.Infof("s+ow-whatsapp-bridge: Accepting pair")
return true
}
ch, err := cli.GetQRChannel(context.Background())
if err != nil {
// This error means that we're already logged in, so ignore it.
if !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
log.Errorf("s+ow-whatsapp-bridge: Failed to get QR channel: %v", err)
}
} else {
go func() {
for evt := range ch {
if evt.Event == "code" {
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
} else {
log.Infof("s+ow-whatsapp-bridge: QR channel result: %s", evt.Event)
}
}
}()
}
cli.AddEventHandler(handler)
err = cli.Connect()
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to connect: %v", err)
return
}
c := make(chan os.Signal, 1)
input := make(chan string)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
defer close(input)
scan := bufio.NewScanner(os.Stdin)
for scan.Scan() {
line := strings.TrimSpace(scan.Text())
if len(line) > 0 {
input <- line
}
}
}()
// s+ow-whatsapp-bridge
// - if 'login' in os.Args, we exit here
for _, arg := range os.Args {
if arg == "login" {
for {
select {
case <-c:
log.Infof("Interrupt received, exiting")
cli.Disconnect()
return
case cmd := <-input:
if len(cmd) == 0 {
log.Infof("Stdin closed, exiting")
cli.Disconnect()
return
}
}
}
}
}
// - if using as cli
args := os.Args[1:]
if len(args) > 0 {
handleCmd(strings.ToLower(args[0]), args[1:])
return
}
// - else, "normal" operation:
// - - read file ~/.cache/s+ow/message
sharetext, err := os.ReadFile(sharetextPath)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to open file: %v", err)
return
}
// - - read JID targets from stdin
targets := <-input
split := strings.Split(targets, ",")
for _, target := range split {
// strip whitespace
target = strings.TrimSpace(target)
// check if prefixed with "wa:"
if strings.HasPrefix(target, "wa:") {
// send message to group
recipient, ok := parseJID(target[3:])
if !ok {
return
}
// reference the "send" case in handleCmd as a single source of truth
msg := &waProto.Message{Conversation: proto.String(strings.TrimSpace(string(sharetext)))}
resp, err := cli.SendMessage(context.Background(), recipient, msg)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Error sending message: %v", err)
} else {
log.Infof("s+ow-whatsapp-bridge: Message sent (server timestamp: %s)", resp.Timestamp)
}
}
}
log.Infof("s+ow-whatsapp-bridge: Exiting")
cli.Disconnect()
return
}
func parseJID(arg string) (types.JID, bool) {
if arg[0] == '+' {
arg = arg[1:]
}
if !strings.ContainsRune(arg, '@') {
return types.NewJID(arg, types.DefaultUserServer), true
}
recipient, err := types.ParseJID(arg)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Invalid JID %s: %v", arg, err)
return recipient, false
} else if recipient.User == "" {
log.Errorf("s+ow-whatsapp-bridge: Invalid JID %s: no server specified", arg)
return recipient, false
}
return recipient, true
}
func handleCmd(cmd string, args []string) {
// s+ow-whatsapp-bridge: we only need the bare minimum:
// - account-related: pair-phone, logout, reconnect
// - chat-related: list, send
switch cmd {
case "pair-phone":
if len(args) < 1 {
log.Errorf("s+ow-whatsapp-bridge: Usage: pair-phone <number>")
return
}
linkingCode, err := cli.PairPhone(args[0], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
if err != nil {
panic(err)
}
fmt.Println("Linking code:", linkingCode)
case "reconnect":
cli.Disconnect()
err := cli.Connect()
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to connect: %v", err)
}
case "logout":
err := cli.Logout()
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Error logging out: %v", err)
} else {
log.Infof("s+ow-whatsapp-bridge: Successfully logged out")
}
case "list":
groups, err := cli.GetJoinedGroups()
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to get group list: %v", err)
} else {
for _, group := range groups {
fmt.Printf("%s\t\t%s\n", group.JID, group.Name)
}
}
case "send":
if len(args) < 2 {
log.Errorf("s+ow-whatsapp-bridge: Usage: send <jid> <text>")
return
}
recipient, ok := parseJID(args[0])
if !ok {
return
}
msg := &waProto.Message{Conversation: proto.String(strings.Join(args[1:], " "))}
resp, err := cli.SendMessage(context.Background(), recipient, msg)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Error sending message: %v", err)
} else {
log.Infof("s+ow-whatsapp-bridge: Message sent (server timestamp: %s)", resp.Timestamp)
}
}
}
var historySyncID int32
var startupTime = time.Now().Unix()
func handler(rawEvt interface{}) {
switch evt := rawEvt.(type) {
case *events.AppStateSyncComplete:
if len(cli.Store.PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
err := cli.SendPresence(types.PresenceAvailable)
if err != nil {
log.Warnf("Failed to send available presence: %v", err)
} else {
log.Infof("s+ow-whatsapp-bridge: Marked self as available")
}
}
case *events.Connected, *events.PushNameSetting:
if len(cli.Store.PushName) == 0 {
return
}
// Send presence available when connecting and when the pushname is changed.
// This makes sure that outgoing messages always have the right pushname.
err := cli.SendPresence(types.PresenceAvailable)
if err != nil {
log.Warnf("Failed to send available presence: %v", err)
} else {
log.Infof("s+ow-whatsapp-bridge: Marked self as available")
}
case *events.StreamReplaced:
os.Exit(0)
case *events.Message:
metaParts := []string{fmt.Sprintf("pushname: %s", evt.Info.PushName), fmt.Sprintf("timestamp: %s", evt.Info.Timestamp)}
if evt.Info.Type != "" {
metaParts = append(metaParts, fmt.Sprintf("type: %s", evt.Info.Type))
}
if evt.Info.Category != "" {
metaParts = append(metaParts, fmt.Sprintf("category: %s", evt.Info.Category))
}
if evt.IsViewOnce {
metaParts = append(metaParts, "view once")
}
if evt.IsViewOnce {
metaParts = append(metaParts, "ephemeral")
}
if evt.IsViewOnceV2 {
metaParts = append(metaParts, "ephemeral (v2)")
}
if evt.IsDocumentWithCaption {
metaParts = append(metaParts, "document with caption")
}
if evt.IsEdit {
metaParts = append(metaParts, "edit")
}
log.Infof("s+ow-whatsapp-bridge: Received message %s from %s (%s): %+v", evt.Info.ID, evt.Info.SourceString(), strings.Join(metaParts, ", "), evt.Message)
if evt.Message.GetPollUpdateMessage() != nil {
decrypted, err := cli.DecryptPollVote(evt)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to decrypt vote: %v", err)
} else {
log.Infof("s+ow-whatsapp-bridge: Selected options in decrypted vote:")
for _, option := range decrypted.SelectedOptions {
log.Infof("s+ow-whatsapp-bridge: - %X", option)
}
}
} else if evt.Message.GetEncReactionMessage() != nil {
decrypted, err := cli.DecryptReaction(evt)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to decrypt encrypted reaction: %v", err)
} else {
log.Infof("s+ow-whatsapp-bridge: Decrypted reaction: %+v", decrypted)
}
}
img := evt.Message.GetImageMessage()
if img != nil {
data, err := cli.Download(img)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to download image: %v", err)
return
}
exts, _ := mime.ExtensionsByType(img.GetMimetype())
savePath := fmt.Sprintf("%s%s", evt.Info.ID, exts[0])
err = os.WriteFile(savePath, data, 0600)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to save image: %v", err)
return
}
log.Infof("s+ow-whatsapp-bridge: Saved image in message to %s", savePath)
}
case *events.Receipt:
if evt.Type == types.ReceiptTypeRead || evt.Type == types.ReceiptTypeReadSelf {
log.Infof("s+ow-whatsapp-bridge: %v was read by %s at %s", evt.MessageIDs, evt.SourceString(), evt.Timestamp)
} else if evt.Type == types.ReceiptTypeDelivered {
log.Infof("s+ow-whatsapp-bridge: %s was delivered to %s at %s", evt.MessageIDs[0], evt.SourceString(), evt.Timestamp)
}
case *events.Presence:
if evt.Unavailable {
if evt.LastSeen.IsZero() {
log.Infof("s+ow-whatsapp-bridge: %s is now offline", evt.From)
} else {
log.Infof("s+ow-whatsapp-bridge: %s is now offline (last seen: %s)", evt.From, evt.LastSeen)
}
} else {
log.Infof("s+ow-whatsapp-bridge: %s is now online", evt.From)
}
case *events.HistorySync:
id := atomic.AddInt32(&historySyncID, 1)
fileName := fmt.Sprintf("history-%d-%d.json", startupTime, id)
file, err := os.OpenFile(fileName, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to open file to write history sync: %v", err)
return
}
enc := json.NewEncoder(file)
enc.SetIndent("", " ")
err = enc.Encode(evt.Data)
if err != nil {
log.Errorf("s+ow-whatsapp-bridge: Failed to write history sync: %v", err)
return
}
log.Infof("s+ow-whatsapp-bridge: Wrote history sync to %s", fileName)
_ = file.Close()
case *events.AppState:
log.Debugf("s+ow-whatsapp-bridge: App state event: %+v / %+v", evt.Index, evt.SyncActionValue)
case *events.KeepAliveTimeout:
log.Debugf("s+ow-whatsapp-bridge: Keepalive timeout event: %+v", evt)
case *events.KeepAliveRestored:
log.Debugf("s+ow-whatsapp-bridge: Keepalive restored")
case *events.Blocklist:
log.Infof("s+ow-whatsapp-bridge: Blocklist event: %+v", evt)
}
}

View file

@ -0,0 +1,4 @@
mkdir -p "$HOME/.local/bin"
pkg install golang
go build
mv spow_whatsapp_bridge "$HOME/.local/bin/s+ow-whatsapp-bridge"

View file

@ -0,0 +1,24 @@
#!/bin/sh
failures=0
ORI_HASH=$(md5sum < bridge.go)
FMT_HASH=$(gofmt bridge.go | md5sum)
if ! [ "$FMT_HASH" = "$ORI_HASH" ]; then
printf "formatted file (%s) is not the same as the original file (%s)" "$FMT_HASH" "$ORI_HASH"
failures=$((failures + 1))
else
printf "formatted file is same as original file - %s (yay!)" "$FMT_HASH"
fi
go vet bridge.go
failures=$((failures + $?))
golint bridge.go
failures=$((failures + $?))
if [ $failures -eq 0 ]; then
printf "\n\nall checks okay! (❁´◡\`❁)\n"
else
printf "\n\nsome checks failed...\n"
fi
exit $failures

View file

@ -0,0 +1,85 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"gomod2nix": {
"inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1722589758,
"narHash": "sha256-sbbA8b6Q2vB/t/r1znHawoXLysCyD4L/6n6/RykiSnA=",
"owner": "nix-community",
"repo": "gomod2nix",
"rev": "4e08ca09253ef996bd4c03afa383b23e35fe28a1",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "gomod2nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1724224976,
"narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c374d94f1536013ca8e92341b540eba4c22f9c62",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"gomod2nix": "gomod2nix",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -0,0 +1,116 @@
{
description = "development environment for surplus on wheels: WhatsApp Bridge";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
gomod2nix = {
url = "github:nix-community/gomod2nix";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
};
outputs = { self, nixpkgs, flake-utils, gomod2nix }:
flake-utils.lib.eachDefaultSystem (system:
let
bridge = {
name = "spow-whatsapp-bridge";
version = "2.2024.34";
};
pkgs = import nixpkgs {
system = system;
overlays = [
gomod2nix.overlays.default
];
config = {
android_sdk.accept_license = true;
allowUnfree = true;
};
};
sdk = (pkgs.androidenv.composeAndroidPackages {
includeSources = false;
includeSystemImages = false;
includeEmulator = false;
includeNDK = true;
ndkVersions = ["26.3.11579264"];
}).androidsdk;
androidClang = (
"${sdk}/libexec/android-sdk/ndk-bundle/toolchains/llvm/prebuilt/"
+
(
{
"x86_64-darwin" = "darwin-x86_64";
"x86_64-linux" = "linux-x86_64";
}."${system}" or
(throw "the android ndk does not support your platform... (`) (apple silicon users see https://github.com/android/ndk/issues/1299)")
)
+
"/bin/aarch64-linux-android30-clang"
);
bridgeBuildTermux = pkgs.buildGoApplication {
pname = bridge.name;
version = bridge.version;
go = pkgs.go_1_22;
src = ./.;
modules = ./gomod2nix.toml;
buildInputs = [ sdk ];
buildPhase = ''
runHook preBuild
mkdir -p $out/bin
CC="${androidClang}" CGO_ENABLED=1 GOOS=android GOARCH=arm64 go build -o $out/bin
runHook postBuild
'';
};
bridgeBuildNative = pkgs.buildGoApplication {
pname = bridge.name;
version = bridge.version;
go = pkgs.go_1_22;
src = ./.;
modules = ./gomod2nix.toml;
};
in
with pkgs; {
# nix develop
devShells.default = mkShell {
buildInputs = [
go
golint
gomod2nix.packages.${system}.default
];
};
# nix build .#termux
packages.termux = stdenvNoCC.mkDerivation {
pname = bridge.name;
version = bridge.version;
src = bridgeBuildTermux;
installPhase = ''
mkdir -p $out
cp $src/bin/$pname $out/$pname
'';
};
# nix build
packages.default = stdenvNoCC.mkDerivation {
pname = bridge.name;
version = bridge.version;
src = bridgeBuildNative;
installPhase = ''
mkdir -p $out
cp $src/bin/$pname $out/$pname
'';
};
}
);
}

View file

@ -0,0 +1,26 @@
module forge.joshwel.co/mark/surplus/src/spow-whatsapp-bridge
go 1.22.3
require (
github.com/mattn/go-sqlite3 v1.14.22
github.com/mdp/qrterminal/v3 v3.2.0
go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7
google.golang.org/protobuf v1.34.2
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/rs/zerolog v1.33.0 // indirect
go.mau.fi/libsignal v0.1.1 // indirect
go.mau.fi/util v0.7.0 // indirect
golang.org/x/crypto v0.26.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/term v0.23.0 // indirect
rsc.io/qr v0.2.0 // indirect
)

View file

@ -0,0 +1,85 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk=
github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.mau.fi/libsignal v0.1.0 h1:vAKI/nJ5tMhdzke4cTK1fb0idJzz1JuEIpmjprueC+c=
go.mau.fi/libsignal v0.1.0/go.mod h1:R8ovrTezxtUNzCQE5PH30StOQWWeBskBsWE55vMfY9I=
go.mau.fi/libsignal v0.1.1 h1:m/0PGBh4QKP/I1MQ44ti4C0fMbLMuHb95cmDw01FIpI=
go.mau.fi/libsignal v0.1.1/go.mod h1:QLs89F/OA3ThdSL2Wz2p+o+fi8uuQUz0e1BRa6ExdBw=
go.mau.fi/util v0.4.2 h1:RR3TOcRHmCF9Bx/3YG4S65MYfa+nV6/rn8qBWW4Mi30=
go.mau.fi/util v0.4.2/go.mod h1:PlAVfUUcPyHPrwnvjkJM9UFcPE7qGPDJqk+Oufa1Gtw=
go.mau.fi/util v0.5.0 h1:8yELAl+1CDRrwGe9NUmREgVclSs26Z68pTWePHVxuDo=
go.mau.fi/util v0.5.0/go.mod h1:DsJzUrJAG53lCZnnYvq9/mOyLuPScWwYhvETiTrpdP4=
go.mau.fi/util v0.6.0 h1:W6SyB3Bm/GjenQ5iq8Z8WWdN85Gy2xS6L0wmnR7SVjg=
go.mau.fi/util v0.6.0/go.mod h1:ljYdq3sPfpICc3zMU+/mHV/sa4z0nKxc67hSBwnrk8U=
go.mau.fi/util v0.7.0 h1:l31z+ivrSQw+cv/9eFebEqtQW2zhxivGypn+JT0h/ws=
go.mau.fi/util v0.7.0/go.mod h1:bWYreIoTULL/UiRbZdfddPh7uWDFW5yX4YCv5FB0eE0=
go.mau.fi/whatsmeow v0.0.0-20240603101645-64bc969fbe78 h1:zST/E2cOjQEjXuis0miwSd20Uf+ffdJna6QefQyxEcc=
go.mau.fi/whatsmeow v0.0.0-20240603101645-64bc969fbe78/go.mod h1:0+65CYaE6r4dWzr0dN8i+UZKy0gIfJ79VuSqIl0nKRM=
go.mau.fi/whatsmeow v0.0.0-20240619210240-329c2336a6f1 h1:gpFEqwk7WtbF/8HaOMASKE6JvxWqhTmaR0CqoPpoly8=
go.mau.fi/whatsmeow v0.0.0-20240619210240-329c2336a6f1/go.mod h1:0+65CYaE6r4dWzr0dN8i+UZKy0gIfJ79VuSqIl0nKRM=
go.mau.fi/whatsmeow v0.0.0-20240716084021-eb41d1f09552 h1:3cI+n5D79nOlS3hef6PD1D8wkXEyxSIW0mvotE8ymVE=
go.mau.fi/whatsmeow v0.0.0-20240716084021-eb41d1f09552/go.mod h1:BhHKalSq0qNtSCuGIUIvoJyU5KbT4a7k8DQ5yw1Ssk4=
go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7 h1:Aa4uov0rM0SQQ7Fc/TZZpmQEGksie2SVTv/UuCJwViI=
go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7/go.mod h1:BhHKalSq0qNtSCuGIUIvoJyU5KbT4a7k8DQ5yw1Ssk4=
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=

View file

@ -0,0 +1,54 @@
schema = 3
[mod]
[mod."filippo.io/edwards25519"]
version = "v1.1.0"
hash = "sha256-9ACANrgWZSd5HYPfDZHY8DVbPSC9LOMgy8deq3rDOoc="
[mod."github.com/google/uuid"]
version = "v1.6.0"
hash = "sha256-VWl9sqUzdOuhW0KzQlv0gwwUQClYkmZwSydHG2sALYw="
[mod."github.com/gorilla/websocket"]
version = "v1.5.3"
hash = "sha256-vTIGEFMEi+30ZdO6ffMNJ/kId6pZs5bbyqov8xe9BM0="
[mod."github.com/mattn/go-colorable"]
version = "v0.1.13"
hash = "sha256-qb3Qbo0CELGRIzvw7NVM1g/aayaz4Tguppk9MD2/OI8="
[mod."github.com/mattn/go-isatty"]
version = "v0.0.20"
hash = "sha256-qhw9hWtU5wnyFyuMbKx+7RB8ckQaFQ8D+8GKPkN3HHQ="
[mod."github.com/mattn/go-sqlite3"]
version = "v1.14.22"
hash = "sha256-CWF2Hjg43658NhaePWbGzS19gHJXjuTroG5c0W3hgYQ="
[mod."github.com/mdp/qrterminal/v3"]
version = "v3.2.0"
hash = "sha256-2ZcpLFu6P+a3qHH32uiFKUwzgza1NF0Bmayl41GQCEI="
[mod."github.com/rs/zerolog"]
version = "v1.33.0"
hash = "sha256-jT/Y/izhZiCdrDbC/ty83FGs8UQavTU+OW03O4vKFkY="
[mod."go.mau.fi/libsignal"]
version = "v0.1.0"
hash = "sha256-hSZQkw/0eV5Y0pj1N+idYuKb/jtiw/qTfaOGdYCXmn0="
[mod."go.mau.fi/util"]
version = "v0.4.2"
hash = "sha256-o/d7Wd+2byFxmVxjl5o/AAUO/2d12vzItq6H5yUtcow="
[mod."go.mau.fi/whatsmeow"]
version = "v0.0.0-20240603101645-64bc969fbe78"
hash = "sha256-MBDcxTHM+ZxxzIrendWWEhNdkPA7cLgkduC424+j+fU="
[mod."golang.org/x/crypto"]
version = "v0.24.0"
hash = "sha256-wpxJApwSmmn9meVdpFdOU0gzeJbIXcKuFfYUUVogSss="
[mod."golang.org/x/net"]
version = "v0.26.0"
hash = "sha256-WfY33QERNbcIiDkH3+p2XGrAVqvWBQfc8neUt6TH6dQ="
[mod."golang.org/x/sys"]
version = "v0.21.0"
hash = "sha256-gapzPWuEqY36V6W2YhIDYR49sEvjJRd7bSuf9K1f4JY="
[mod."golang.org/x/term"]
version = "v0.21.0"
hash = "sha256-zRm7uPBM1+TJkODYHkk/BtN3la5QAaSgslE2hSTm27Y="
[mod."google.golang.org/protobuf"]
version = "v1.34.2"
hash = "sha256-nMTlrDEE2dbpWz50eQMPBQXCyQh4IdjrTIccaU0F3m0="
[mod."rsc.io/qr"]
version = "v0.2.0"
hash = "sha256-I3fAJwwZhIrgBbCjWvIElAE9JqG2y59KRBc78EYi3RM="

View file

@ -0,0 +1,5 @@
#!/bin/sh
# surplus on wheels: WhatsApp Bridge: installation and updater script
set -e
echo TODO

View file

@ -0,0 +1,2 @@
SPOW_TELEGRAM_API_HASH="" SPOW_TELEGRAM_API_ID="" s+ow-telegram-bridge
s+ow-whatsapp-bridge

View file

@ -0,0 +1,13 @@
# surplus on wheels
surplus on wheels (s+ow) is a pure shell script to get your location using
[termux-location](https://wiki.termux.com/wiki/Termux-location), process it through surplus, and
send it to messaging service or wherever using “bridges”
surplus was made to emulate sending your location through the iOS Shortcuts app, and surplus on
wheels complements it by running surplus automatically using a cron job.
(but using it manually also works!)
see <https://surplus.joshwel.co/onwheels/>
or [/docs/onwheels/index.md](../../docs/onwheels/index.md)
for more information and documentation

View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View file

@ -0,0 +1,21 @@
#!/bin/sh
failures=0
FMT_HASH=$(shfmt s+ow | md5sum)
ORI_HASH=$(md5sum < s+ow)
if ! [ "$FMT_HASH" = "$ORI_HASH" ]; then
printf "formatted file (%s) is not the same as the original file (%s)" "$FMT_HASH" "$ORI_HASH"
failures=$((failures + 1))
else
printf "formatted file is same as original file - %s (yay!)" "$FMT_HASH"
fi
shellcheck s+ow
failures=$((failures + $?))
if [ $failures -eq 0 ]; then
printf "\n\nall checks okay! (❁´◡\`❁)\n"
else
printf "\n\nsome checks failed...\n"
fi
exit $failures

View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1710146030,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1718318537,
"narHash": "sha256-4Zu0RYRcAY/VWuu6awwq4opuiD//ahpc2aFHg2CWqFY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "e9ee548d90ff586a6471b4ae80ae9cfcbceb3420",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -0,0 +1,20 @@
{
description = "development environment for surplus on wheels";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
with pkgs; {
devShells.default = mkShellNoCC {
buildInputs = [ shfmt shellcheck ];
};
}
);
}

View file

@ -0,0 +1,37 @@
#!/bin/sh
# surplus on wheels: termux installation script
set -e
# get packages
yes | pkg upgrade
yes | pkg install python cronie termux-api termux-services wget
# install pipx and surplus
pip install pipx
pipx install surplus
# install s+ow
mkdir -p ~/.local/bin/
if ping -c 1 surplus.joshwel.co ; then
wget -O ~/.local/bin/s+ow https://surplus.joshwel.co/spow.sh
else
wget -O ~/.local/bin/s+ow https://raw.githubusercontent.com/markjoshwel/surplus/main/src/surplus-on-wheels/s+ow
fi
chmod +x ~/.local/bin/s+ow
# setup path
echo "export PATH=\$PATH:\$HOME/.local/bin/" >> ~/.profile
printf "
----- done! -----
if you're going to set a cron job up:
1. restart termux
2. run crontab -e
3. add \"59 * * * * bash -l -c \"(SPOW_TARGETS="" SPOW_CRON=y s+ow)\"\"
(remember to minimally fill in the SPOW_TARGETS variable)
else, surplus on wheels has been set up!
"

550
src/surplus-on-wheels/s+ow Normal file
View file

@ -0,0 +1,550 @@
#!/bin/sh
# surplus on wheels (s+ow) - a pure shell script to run surplus with mdtest using the termux-api
# ------------------------
# This is free and unencumbered software released into the public domain.
#
# Anyone is free to copy, modify, publish, use, compile, sell, or
# distribute this software, either in source code form or as a compiled
# binary, for any purpose, commercial or non-commercial, and by any
# means.
#
# In jurisdictions that recognize copyright laws, the author or authors
# of this software dedicate any and all copyright interest in the
# software to the public domain. We make this dedication for the benefit
# of the public at large and to the detriment of our heirs and
# successors. We intend this dedication to be an overt act of
# relinquishment in perpetuity of all present and future rights to this
# software under copyright law.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
# OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
# ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# For more information, please refer to <http://unlicense.org/>
SURPLUS_CMD_DEFAULT="surplus -td"
SURPLUS_CMD=${SURPLUS_CMD:-$SURPLUS_CMD_DEFAULT}
LOCATION_CMD_DEFAULT="termux-location"
LOCATION_CMD=${LOCATION_CMD:-$LOCATION_CMD_DEFAULT}
# shellcheck disable=SC2059
LOCATION_FALLBACK=${LOCATION_FALLBACK:-"%d%d%d\nSingapore?"}
# shellcheck disable=SC2269
LOCATION_PRIORITISE_NETWORK="$LOCATION_PRIORITISE_NETWORK"
LOCATION_TIMEOUT=${LOCATION_TIMEOUT:-50}
# shellcheck disable=SC2269
SPOW_TARGETS="$SPOW_TARGETS"
SPOW_CACHE_DIR="$HOME/.cache/s+ow"
SPOW_BRIDGES="$HOME/.s+ow-bridges"
SPOW_CRON=${SPOW_CRON:-n}
SPOW_PRIVATE=${SPOW_PRIVATE:-n}
# per-tool session logs
SPOW_NETLC_OUT="$SPOW_CACHE_DIR/location.net.json" # honours SPOW_PRIVATE (cleared after use)
SPOW_GPSLC_OUT="$SPOW_CACHE_DIR/location.gps.json" # honours SPOW_PRIVATE (cleared after use)
SPOW_LOCTN_OUT="$SPOW_CACHE_DIR/location.json" # honours SPOW_PRIVATE (cleared after use)
SPOW_SPLUS_OUT="$SPOW_CACHE_DIR/surplus.out.log" # honours SPOW_PRIVATE (cleared after use)
SPOW_SPLUS_ERR="$SPOW_CACHE_DIR/surplus.err.log" # honours SPOW_PRIVATE (set to /dev/null + cleared after use)
# per-session collated logs
SPOW_SESH_OUT="$SPOW_CACHE_DIR/out.log" # honours SPOW_PRIVATE (set to /dev/null)
SPOW_SESH_ERR="$SPOW_CACHE_DIR/err.log" # honours SPOW_PRIVATE (set to /dev/null)
# per-week collated logs
SPOW_WEEK_PRE="$SPOW_CACHE_DIR/$(date +%Y)W$(date +"%V")"
SPOW_WEEK_OUT="$SPOW_WEEK_PRE.out.log"
SPOW_WEEK_ERR="$SPOW_WEEK_PRE.err.log"
# last successful surplus output
SPOW_LAST_OUT="$SPOW_CACHE_DIR/last"
# message to be sent
SPOW_MESSAGE="$SPOW_CACHE_DIR/message" # honours SPOW_PRIVATE (cleared after use)
# list of fakes
SPOW_FAKE_OUT="$SPOW_CACHE_DIR/fake"
# check for network location priority
if [ "$LOCATION_PRIORITISE_NETWORK" = "n" ]; then
LOCATION_PRIORITISE_NETWORK=""
fi
# check for cron status
if [ "$SPOW_CRON" = "n" ]; then
SPOW_CRON=""
fi
# check if running in 'private' mode
if [ "$SPOW_PRIVATE" = "n" ]; then
SPOW_PRIVATE=""
fi
# extract command names for checking
TERMUX_EXE=$(echo "$TERMUX_CMD" | awk '{print $1}')
SURPLUS_EXE=$(echo "$SURPLUS_CMD" | awk '{print $1}')
# ensure commands exist
if ! command -v "$SURPLUS_EXE" >/dev/null 2>&1; then
if [ "$SURPLUS_EXE" = "surplus" ]; then
printf "s+ow: error: surplus is not installed.\ninstall it with 'pip install surplus'. see <https://surplus.joshwel.co> for more information.\n"
else
printf "s+ow: error: custom surplus command '%s' is not accessible" "$SURPLUS_EXE"
fi
exit 2
fi
if ! command -v "$TERMUX_EXE" >/dev/null 2>&1; then
if [ "$TERMUX_EXE" = $LOCATION_CMD_DEFAULT ]; then
printf "s+ow: error: termux-location is not installed.\ninstall it with 'pkg install termux-api' and with installing the termux:api app from the play store or f-droid.\n"
else
printf "s+ow: error: custom location command '%s' is not accessible" "$TERMUX_EXE"
fi
exit 1
fi
# ensure directories
mkdir -p "$SPOW_CACHE_DIR"
# create new session logs
rm -f "$SPOW_LOCTN_OUT" "$SPOW_SPLUS_OUT" "$SPOW_SPLUS_ERR" \
"$SPOW_SESH_OUT" "$SPOW_SESH_ERR"
touch "$SPOW_NETLC_OUT" "$SPOW_GPSLC_OUT" "$SPOW_LOCTN_OUT" \
"$SPOW_SPLUS_OUT" "$SPOW_SPLUS_ERR" \
"$SPOW_SESH_OUT" "$SPOW_SESH_ERR" \
"$SPOW_WEEK_OUT" "$SPOW_WEEK_ERR" \
"$SPOW_BRIDGES" "$SPOW_FAKE_OUT"
# disable logs if private
if [ -n "$SPOW_PRIVATE" ]; then
SPOW_SESH_OUT="/dev/null"
SPOW_SESH_ERR="/dev/null"
SPOW_SPLUS_ERR="/dev/null"
fi
# 0 is nominal
# 1 is an termux-location error
# 2 is a surplus error
# 3 is a bridge/message send error
status=0
bridge_failures=0
bridge_returns=""
locate() {
# spawn termux-location processes
(
$LOCATION_CMD -p "network" >"$SPOW_NETLC_OUT"
if [ -s "$SPOW_NETLC_OUT" ]; then
printf "net" | tee -a "$SPOW_SESH_ERR"
else
printf "net?" | tee -a "$SPOW_SESH_ERR"
fi
cat "$SPOW_NETLC_OUT" >>"$SPOW_SESH_OUT"
) &
tl_net_pid="$!"
sleep 1
(
$LOCATION_CMD -p "gps" >"$SPOW_GPSLC_OUT"
if [ -s "$SPOW_GPSLC_OUT" ]; then
printf "gps" | tee -a "$SPOW_SESH_ERR"
else
printf "gps?" | tee -a "$SPOW_SESH_ERR"
fi
cat "$SPOW_GPSLC_OUT" >>"$SPOW_SESH_OUT"
) &
tl_gps_pid="$!"
# wait until timeout or both finished
printf "running '%s'" "$LOCATION_CMD" | tee -a "$SPOW_SESH_ERR"
while [ "$LOCATION_TIMEOUT" -gt 0 ]; do
# get process statuses
kill -0 "$tl_net_pid" >/dev/null 2>&1
tl_net_status="$?"
kill -0 "$tl_gps_pid" >/dev/null 2>&1
tl_gps_status="$?"
# break if both finished
if [ "$tl_net_status" -eq 1 ] && [ "$tl_gps_status" -eq 1 ]; then
break
fi
# exception: if network is prioritised: just use that
if [ "$tl_net_status" -eq 1 ] && [ -n "$LOCATION_PRIORITISE_NETWORK" ]; then
# break only if theres an actual response
if [ -s "$SPOW_NETLC_OUT" ]; then
break
fi
# else just keep on waiting for gps to finish
fi
sleep 1
printf "." | tee -a "$SPOW_SESH_ERR"
LOCATION_TIMEOUT=$((LOCATION_TIMEOUT - 1))
done
if [ "$LOCATION_TIMEOUT" -eq 0 ]; then
printf " errored (timeout)\n" | tee -a "$SPOW_SESH_ERR"
else
printf " nominal\n" | tee -a "$SPOW_SESH_ERR"
fi
# check outputs
printf "determining output: " | tee -a "$SPOW_SESH_ERR"
if [ -s "$SPOW_NETLC_OUT" ] && [ -s "$SPOW_GPSLC_OUT" ]; then
printf "both succeeded, "
acc_net="$(grep "\"accuracy\"" <"$SPOW_NETLC_OUT" | awk -F ': ' '{print $2}' | tr -d ',')"
acc_gps="$(grep "\"accuracy\"" <"$SPOW_GPSLC_OUT" | awk -F ': ' '{print $2}' | tr -d ',')"
# compare accuracy
if awk -v n1="$acc_net" -v n2="$acc_gps" 'BEGIN { if (n1 < n2) exit 0; else exit 1; }'; then
printf "choosing network (%s < %s)" "$acc_net" "$acc_gps" | tee -a "$SPOW_SESH_ERR"
cat "$SPOW_NETLC_OUT" >"$SPOW_LOCTN_OUT"
else
printf "choosing gps (%s < %s)" "$acc_gps" "$acc_net" | tee -a "$SPOW_SESH_ERR"
cat "$SPOW_GPSLC_OUT" >"$SPOW_LOCTN_OUT"
fi
cat "$SPOW_GPSLC_OUT" >"$SPOW_LOCTN_OUT"
else
# one or none succeeded
if [ -s "$SPOW_NETLC_OUT" ]; then
if [ -n "$LOCATION_PRIORITISE_NETWORK" ]; then
printf "using network (prioritised)" | tee -a "$SPOW_SESH_ERR"
else
printf "using network" | tee -a "$SPOW_SESH_ERR"
fi
cat "$SPOW_NETLC_OUT" >"$SPOW_LOCTN_OUT"
fi
if [ -s "$SPOW_GPSLC_OUT" ]; then
printf "using gps" | tee -a "$SPOW_SESH_ERR"
cat "$SPOW_GPSLC_OUT" >"$SPOW_LOCTN_OUT"
fi
fi
if [ ! -s "$SPOW_LOCTN_OUT" ]; then
printf "none (error)" | tee -a "$SPOW_SESH_ERR"
fi
printf "\n" | tee -a "$SPOW_SESH_ERR"
}
gensharetext() {
$SURPLUS_CMD "$1" >"$SPOW_SPLUS_OUT" 2>"$SPOW_SPLUS_ERR"
ret="$?"
cat "$SPOW_SPLUS_OUT" >>"$SPOW_SESH_OUT"
cat "$SPOW_SPLUS_ERR" >>"$SPOW_SESH_ERR"
return "$ret"
}
send() {
# $1 is sharetext
# use fake if any
fake_first=""
fake_rest=""
if [ -n "$(cat "$SPOW_FAKE_OUT")" ]; then
inside_first_group=false
first_group_done=false
while IFS= read -r line; do
# skip preceding empty lines
if [ -z "$line" ] && [ "$inside_first_group" = false ] && [ -z "$fake_first" ]; then
continue
fi
# if not empty and not inside first group, then we are inside first group
if [ -n "$line" ] && [ "$inside_first_group" = false ] && [ "$first_group_done" = false ]; then
inside_first_group=true
# if empty and inside first group, then we are outside first group
elif [ -z "$line" ] && [ "$inside_first_group" = true ]; then
inside_first_group=false
first_group_done=true
# if empty and outside first group but first group is done, add newline to fake_rest
elif [ -z "$line" ] && [ "$inside_first_group" = false ] && [ "$first_group_done" = true ]; then
fake_rest="$fake_rest\n"
fi
# append to fake_first (if message_fake is not empty)
if [ "$inside_first_group" = true ]; then
if [ -z "$fake_first" ]; then
fake_first="$line"
else
fake_first="$(printf "%s\n%s" "$fake_first" "$line")"
fi
# append to fake_rest (if message_fake is not empty)
else
if [ -z "$fake_rest" ]; then
fake_rest="$line"
else
fake_rest="$(printf "%s\n%s" "$fake_rest" "$line")"
fi
fi
done <"$SPOW_FAKE_OUT"
if [ -n "$fake_first" ]; then
printf "%s\n" "$fake_rest" >"$SPOW_FAKE_OUT"
fi
fi
# choose what message to use
message=""
if [ -n "$fake_first" ]; then
echo using fake
message="$fake_first"
else
echo using message
message="$1"
fi
# store message
printf "%s\n" "$message" >"$SPOW_MESSAGE"
cat "$SPOW_MESSAGE" >>"$SPOW_SESH_OUT"
cat "$SPOW_MESSAGE" >>"$SPOW_SESH_ERR"
# run bridges
if [ -f "$SPOW_BRIDGES" ]; then
# run commands in bridge file
while IFS= read -r command; do
# skip command if its actually a comment
if [ "$(printf "%s" "$command" | head -c 1)" = "#" ]; then
continue
fi
# run bridge
echo "$command"
echo "$SPOW_TARGETS" | eval "$command" >>"$SPOW_SESH_ERR" 2>>"$SPOW_SESH_ERR"
bridge_return="$?"
# check if bridge failed
if [ ! "$bridge_return" -eq 0 ]; then
bridge_failures=$((bridge_failures + 1))
fi
# store return value
if [ -z "$bridge_returns" ]; then
bridge_returns="$bridge_return"
else
bridge_returns="$bridge_returns, $bridge_return"
fi
done <"$SPOW_BRIDGES"
else
printf "s+ow: warning: no '%s' file; message is not sent.\n" "$SPOW_BRIDGES"
termux-notification \
--priority "default" \
--title "surplus on wheels: No bridges" \
--content "No '$SPOW_BRIDGES' file; message is not sent."
fi
echo "$bridge_returns" >>"$SPOW_SESH_ERR"
}
notify_start() {
termux-notification \
--priority "min" \
--ongoing \
--id "s+ow" \
--title "surplus on wheels" \
--content "s+ow has started running."
}
notify() {
# $1 is text
# $2 is attempt number (if any)
attempt_text="$1"
if [ $# -eq 2 ]; then
attempt_text="$1 (attempt $2)"
fi
termux-notification \
--priority "min" \
--ongoing \
--id "s+ow" \
--title "surplus on wheels" \
--content "$attempt_text"
}
notify_end() {
# $1 is done tuple
# $2 is sharetext
termux-notification \
--priority "min" \
--id "s+ow" \
--title "surplus on wheels" \
--content "$(printf 'Run has finished. %s\n\n%s' "$1" "$2")"
}
# program functions
run() {
notify_start
printf "[run! stdout (%s)]\n" "$(date)" >>"$SPOW_SESH_OUT"
printf "[run! stderr (%s)]\n" "$(date)" >>"$SPOW_SESH_ERR"
time_run_start="$(date +%s)"
# if cron: wait until its the new hour
if [ -n "$SPOW_CRON" ]; then
notify "Waiting for the 30th second to pass..."
printf "waiting for the 30th second to pass...\n"
while [ "$(date +'%S')" -lt 30 ]; do
printf " %s\n" "$(date)"
sleep 1
done
printf "proceeding\n"
fi
time_locate_start="$(date +%s)"
# termux-location
location=""
for locate_run in 1 2 3; do # run three times in case :p
notify "Running '%s'" "$LOCATION_CMD" "$locate_run"
if [ "$locate_run" -gt "1" ]; then
LOCATION_TIMEOUT=75 locate
else
locate
fi
if [ ! -s "$SPOW_LOCTN_OUT" ]; then
# erroneous: is empty
echo "s+ow: error: failed to get location" | tee -a "$SPOW_SESH_ERR"
status=1
else
# nominal: is not empty
location="$(cat "$SPOW_LOCTN_OUT")"
status=0
break
fi
done
if [ -n "$SPOW_PRIVATE" ]; then
cat /dev/null >"$SPOW_GPSLC_OUT"
cat /dev/null >"$SPOW_NETLC_OUT"
cat /dev/null >"$SPOW_LOCTN_OUT"
fi
time_locate_end="$(date +%s)"
time_locate=$((time_locate_end - time_locate_start))
time_surplus_start="$(date +%s)"
# surplus
printf "running '%s'... " "$SURPLUS_CMD"
notify "Running $SURPLUS_CMD $location"
if [ "$status" -eq 0 ]; then
if gensharetext "$location"; then
# surplus ran nominally
cp "$SPOW_SPLUS_OUT" "$SPOW_LAST_OUT"
status=0
printf "nominal\n"
else
# something happened :^)
status=2
printf "errored\n"
fi
else
printf "skipped\n"
fi
time_surplus_end="$(date +%s)"
time_surplus=$((time_surplus_end - time_surplus_start))
# if cron: wait until its the new hour
if [ -n "$SPOW_CRON" ]; then
notify "Waiting for the new hour..."
printf "waiting until the new hour...\n"
while [ "$(date +'%M')" -eq 59 ]; do
printf " %s\n" "$(date)"
sleep 1
done
printf "proceeding\n"
fi
time_sendmsg_start="$(date +%s)"
# send message
printf "sending message(s)... "
notify "Sending message(s)"
# 0 for freshly made sharetext
# 1 for recycling a last location
# 2 for using fallback template
sent_type=0
bridge_failures=0
sharetext=""
send_error_notif=false
if [ "$status" -eq 0 ]; then
# s+ow has behaved nominally until now, send as per normal
sharetext="$(cat "$SPOW_SPLUS_OUT")"
printf "\n"
send "$sharetext"
else
# something has gone wrong, send an appropriate fallback
sharetext=""
if [ -s "$SPOW_LAST_OUT" ]; then
# use last successful location
sharetext="$(cat "$SPOW_LAST_OUT")"
sent_type=1
printf "using last...\n"
else
# no last location, use fallback
sent_type=2
# shellcheck disable=SC2059
sharetext="$(printf "$LOCATION_FALLBACK" "$status" "$locate_run" "$sent_type")"
printf "using fallback... \n"
fi
send "$sharetext"
# send done info except
printf "(%d, %d, %d, %s<%s>) [lc:%ds sp:%ds]\n" "$status" "$locate_run" "$sent_type" "$bridge_failures" "$bridge_returns" "$time_locate" "$time_surplus" >>"$SPOW_SESH_ERR"
send_error_notif=true
fi
# delete s+ logs
if [ -n "$SPOW_PRIVATE" ]; then
cat /dev/null >"$SPOW_SPLUS_OUT"
cat /dev/null >"$SPOW_SPLUS_ERR"
cat /dev/null >"$SPOW_MESSAGE"
fi
time_sendmsg_end="$(date +%s)"
time_sendmsg=$((time_sendmsg_end - time_sendmsg_start))
time_run_end="$(date +%s)"
time_run=$((time_run_end - time_run_start))
done_info="$(printf "(%d, %d, %d, %s<%s>) [lc:%ds sp:%ds sm:%ds - %ds]\n" "$status" "$locate_run" "$sent_type" "$bridge_failures" "$bridge_returns" "$time_locate" "$time_surplus" "$time_sendmsg" "$time_run")"
echo "done $done_info" | tee -a "$SPOW_SESH_ERR"
# finish
notify_end "$done_info" "$sharetext"
if [ ! "$send_error_notif" = false ]; then
# error notification
termux-notification \
--priority "default" \
--title "surplus on wheels has errored" \
--content "$(printf "\n(%d, %d, %d, %s<%s>)\n[lc:%ds sp:%ds sm:%ds - %ds]\n" "$status" "$locate_run" "$sent_type" "$bridge_failures" "$bridge_returns" "$time_locate" "$time_surplus" "$time_sendmsg" "$time_run")"
fi
}
# script entry
if [ -z "$SPOW_TARGETS" ]; then
echo "s+ow: error: SPOW_TARGETS are not set"
exit 3
fi
run
printf "%s\n\n" "$(cat "$SPOW_SESH_OUT")" >>"$SPOW_WEEK_OUT"
printf "%s\n\n" "$(cat "$SPOW_SESH_ERR")" >>"$SPOW_WEEK_ERR"

View file

@ -0,0 +1,26 @@
#!/bin/sh
SURPLUS_CMD_DEFAULT="surplus --debugp -tp"
SURPLUS_CMD=${SURPLUS_CMD:-$SURPLUS_CMD_DEFAULT}
# parse SURPLUS_CMD to see if "-p" or "--private" is in the args
set -f
# shellcheck disable=SC2086
set -- $SURPLUS_CMD
for arg; do
case "$arg" in
--private)
echo YAY
break
;;
--*)
;;
-*)
if echo "$arg" | grep -q "p"; then
echo YAY
break
fi
;;
esac
done
set +f

8
src/surplus/README.md Normal file
View file

@ -0,0 +1,8 @@
# surplus
surplus (s+) is a Python script to convert [Google Maps Plus Codes](https://maps.google.com/pluscodes/)
to iOS Shortcuts-like shareable text
see <https://surplus.joshwel.co/>
or [/docs/index.md](../../docs/index.md)
for more information and documentation

24
src/surplus/UNLICENCE Normal file
View file

@ -0,0 +1,24 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>

View file

@ -32,24 +32,13 @@ For more information, please refer to <http://unlicense.org/>
# surplus was and would've been a single-file module, but typing is in the way :( # surplus was and would've been a single-file module, but typing is in the way :(
# https://github.com/python/typing/issues/1333 # https://github.com/python/typing/issues/1333
from .surplus import default_geocoder # deprecated, emulation function from .surplus import ( # noqa: F401, TID252
from .surplus import default_reverser # deprecated, emulation function
from .surplus import (
BUILD_BRANCH, BUILD_BRANCH,
BUILD_COMMIT, BUILD_COMMIT,
BUILD_DATETIME, BUILD_DATETIME,
CONNECTION_MAX_RETRIES, CONNECTION_MAX_RETRIES,
CONNECTION_WAIT_SECONDS, CONNECTION_WAIT_SECONDS,
EMPTY_LATLONG, EMPTY_LATLONG,
SHAREABLE_TEXT_LINE_0_KEYS,
SHAREABLE_TEXT_LINE_1_KEYS,
SHAREABLE_TEXT_LINE_2_KEYS,
SHAREABLE_TEXT_LINE_3_KEYS,
SHAREABLE_TEXT_LINE_4_KEYS,
SHAREABLE_TEXT_LINE_5_KEYS,
SHAREABLE_TEXT_LINE_6_KEYS,
SHAREABLE_TEXT_LOCALITY,
SHAREABLE_TEXT_NAMES,
VERSION, VERSION,
VERSION_SUFFIX, VERSION_SUFFIX,
Behaviour, Behaviour,
@ -68,12 +57,12 @@ from .surplus import (
ResultType, ResultType,
StringQuery, StringQuery,
SurplusDefaultGeocoding, SurplusDefaultGeocoding,
SurplusException, SurplusError,
SurplusGeocoderProtocol, SurplusGeocoderProtocol,
SurplusReverserProtocol, SurplusReverserProtocol,
__version__,
cli, cli,
generate_fingerprinted_user_agent, generate_fingerprinted_user_agent,
handle_args,
parse_query, parse_query,
surplus, surplus,
) )

View file

@ -31,6 +31,7 @@ For more information, please refer to <http://unlicense.org/>
from argparse import ArgumentParser from argparse import ArgumentParser
from collections import OrderedDict from collections import OrderedDict
from collections.abc import Callable, Sequence
from copy import deepcopy from copy import deepcopy
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
@ -41,43 +42,43 @@ from json import loads as json_loads
from json.decoder import JSONDecodeError from json.decoder import JSONDecodeError
from platform import platform from platform import platform
from socket import gethostname from socket import gethostname
from sys import exit as sysexit
from sys import stderr, stdin, stdout from sys import stderr, stdin, stdout
from typing import ( from typing import (
TYPE_CHECKING,
Any, Any,
Callable,
Final, Final,
Generic, Generic,
NamedTuple, NamedTuple,
Protocol, Protocol,
Sequence,
TextIO, TextIO,
TypeAlias, TypeAlias,
TypeVar, TypeVar,
) )
from uuid import getnode from uuid import getnode
from geopy import Location as _geopy_Location # type: ignore
from geopy.extra.rate_limiter import RateLimiter as _geopy_RateLimiter # type: ignore from geopy.extra.rate_limiter import RateLimiter as _geopy_RateLimiter # type: ignore
from geopy.geocoders import Nominatim as _geopy_Nominatim # type: ignore from geopy.geocoders import Nominatim as _geopy_Nominatim # type: ignore
from pluscodes import PlusCode as _PlusCode # type: ignore from pluscodes import PlusCode as _PlusCode # type: ignore
from pluscodes import encode as _PlusCode_encode # type: ignore from pluscodes import encode as _encode # type: ignore
from pluscodes.openlocationcode import recoverNearest as _PlusCode_recoverNearest # type: ignore
from pluscodes.validator import Validator as _PlusCode_Validator # type: ignore from pluscodes.validator import Validator as _PlusCode_Validator # type: ignore
from pluscodes.openlocationcode import ( # type: ignore # isort: skip if TYPE_CHECKING:
recoverNearest as _PlusCode_recoverNearest, from geopy import Location as _geopy_Location # type: ignore
)
# constants # constants
VERSION: Final[tuple[int, int, int]] = (2, 2, 0) __version__ = "2024.0.0-beta"
VERSION_SUFFIX: Final[str] = "-local" VERSION: Final[tuple[int, int, int]] = (2024, 0, 0)
VERSION_SUFFIX: Final[str] = "-beta-local"
BUILD_BRANCH: Final[str] = "future" BUILD_BRANCH: Final[str] = "future"
BUILD_COMMIT: Final[str] = "latest" BUILD_COMMIT: Final[str] = "latest"
BUILD_DATETIME: Final[datetime] = datetime.now(timezone(timedelta(hours=8))) # using SGT BUILD_DATETIME: Final[datetime] = datetime.now(timezone(timedelta(hours=8))) # using SGT
CONNECTION_MAX_RETRIES: int = 9 CONNECTION_MAX_RETRIES: int = 9
CONNECTION_WAIT_SECONDS: int = 10 CONNECTION_WAIT_SECONDS: int = 10
LOCALITY_GEOCODER_LEVEL: int = 13 # adjusts geocoder zoom level when LOCALITY_GEOCODER_LEVEL: int = 13 # adjusts geocoder zoom level when
# geocoding latlong into an address # geocoding lat long into an address
# default shareable text line keys # default shareable text line keys
SHAREABLE_TEXT_LINE_0_KEYS: dict[str, tuple[str, ...]] = { SHAREABLE_TEXT_LINE_0_KEYS: dict[str, tuple[str, ...]] = {
@ -215,15 +216,13 @@ SHAREABLE_TEXT_LINE_6_KEYS.update(
), ),
} }
) )
SHAREABLE_TEXT_LINE_SETTINGS.update( SHAREABLE_TEXT_LINE_SETTINGS.update({"IT": deepcopy(SHAREABLE_TEXT_LINE_SETTINGS)["default"]})
{"IT": deepcopy(SHAREABLE_TEXT_LINE_SETTINGS)["default"]}
)
SHAREABLE_TEXT_LINE_SETTINGS["IT"][5] = (" ", False) SHAREABLE_TEXT_LINE_SETTINGS["IT"][5] = (" ", False)
# special per-country key arrangements for MY/Malaysia # special per-country key arrangements for MY/Malaysia
SHAREABLE_TEXT_LINE_4_KEYS.update( SHAREABLE_TEXT_LINE_4_KEYS.update(
{ {
"MY": tuple(), "MY": (),
}, },
) )
SHAREABLE_TEXT_LINE_5_KEYS.update( SHAREABLE_TEXT_LINE_5_KEYS.update(
@ -234,9 +233,7 @@ SHAREABLE_TEXT_LINE_5_KEYS.update(
), ),
}, },
) )
SHAREABLE_TEXT_LINE_SETTINGS.update( SHAREABLE_TEXT_LINE_SETTINGS.update({"MY": deepcopy(SHAREABLE_TEXT_LINE_SETTINGS)["default"]})
{"MY": deepcopy(SHAREABLE_TEXT_LINE_SETTINGS)["default"]}
)
SHAREABLE_TEXT_LINE_SETTINGS["MY"][4] = (" ", False) SHAREABLE_TEXT_LINE_SETTINGS["MY"][4] = (" ", False)
SHAREABLE_TEXT_LINE_SETTINGS["MY"][5] = (" ", True) SHAREABLE_TEXT_LINE_SETTINGS["MY"][5] = (" ", True)
@ -244,30 +241,23 @@ SHAREABLE_TEXT_LINE_SETTINGS["MY"][5] = (" ", True)
# exceptions # exceptions
class SurplusException(Exception): class SurplusError(Exception):
"""base skeleton exception for handling and typing surplus exception classes""" """base skeleton exception for handling and typing surplus exception classes"""
...
class NoSuitableLocationError(SurplusError): ...
class NoSuitableLocationError(SurplusException): class IncompletePlusCodeError(SurplusError): ...
...
class IncompletePlusCodeError(SurplusException): class PlusCodeNotFoundError(SurplusError): ...
...
class PlusCodeNotFoundError(SurplusException): class LatlongParseError(SurplusError): ...
...
class LatlongParseError(SurplusException): class EmptyQueryError(SurplusError): ...
...
class EmptyQueryError(SurplusException):
...
# data structures # data structures
@ -357,7 +347,7 @@ class Result(NamedTuple, Generic[ResultType]):
"""method that returns True if self.error is not None""" """method that returns True if self.error is not None"""
return self.error is None return self.error is None
def cry(self, string: bool = False) -> str: def cry(self, string: bool = False) -> str: # noqa: FBT001, FBT002
""" """
method that raises self.error if is an instance of BaseException, method that raises self.error if is an instance of BaseException,
returns self.error if is an instance of str, or returns an empty string if returns self.error if is an instance of str, or returns an empty string if
@ -438,8 +428,7 @@ class SurplusGeocoderProtocol(Protocol):
exceptions are handled by the caller exceptions are handled by the caller
""" """
def __call__(self, place: str) -> Latlong: def __call__(self, place: str) -> Latlong: ...
...
class SurplusReverserProtocol(Protocol): class SurplusReverserProtocol(Protocol):
@ -478,8 +467,7 @@ class SurplusReverserProtocol(Protocol):
exceptions are handled by the caller exceptions are handled by the caller
""" """
def __call__(self, latlong: Latlong, level: int = 18) -> dict[str, Any]: def __call__(self, latlong: Latlong, level: int = 18) -> dict[str, Any]: ...
...
class PlusCodeQuery(NamedTuple): class PlusCodeQuery(NamedTuple):
@ -496,7 +484,7 @@ class PlusCodeQuery(NamedTuple):
code: str code: str
def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: # noqa: ARG002
""" """
method that returns a latitude-longitude coordinate pair method that returns a latitude-longitude coordinate pair
@ -525,7 +513,7 @@ class PlusCodeQuery(NamedTuple):
), ),
) )
except Exception as exc: except Exception as exc: # noqa: BLE001
return Result[Latlong](EMPTY_LATLONG, error=exc) return Result[Latlong](EMPTY_LATLONG, error=exc)
return Result[Latlong](Latlong(latitude=latitude, longitude=longitude)) return Result[Latlong](Latlong(latitude=latitude, longitude=longitude))
@ -578,7 +566,7 @@ class LocalCodeQuery(NamedTuple):
return Result[str](recovered_pluscode) return Result[str](recovered_pluscode)
except Exception as exc: except Exception as exc: # noqa: BLE001
return Result[str]("", error=exc) return Result[str]("", error=exc)
def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]:
@ -623,7 +611,7 @@ class LatlongQuery(NamedTuple):
latlong: Latlong latlong: Latlong
def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: # noqa: ARG002
""" """
method that returns a latitude-longitude coordinate pair method that returns a latitude-longitude coordinate pair
@ -639,7 +627,7 @@ class LatlongQuery(NamedTuple):
def __str__(self) -> str: def __str__(self) -> str:
"""method that returns string representation of query""" """method that returns string representation of query"""
return f"{str(self.latlong)}" return f"{self.latlong!s}"
class StringQuery(NamedTuple): class StringQuery(NamedTuple):
@ -671,7 +659,7 @@ class StringQuery(NamedTuple):
try: try:
return Result[Latlong](geocoder(self.query)) return Result[Latlong](geocoder(self.query))
except Exception as exc: except Exception as exc: # noqa: BLE001
return Result[Latlong](EMPTY_LATLONG, error=exc) return Result[Latlong](EMPTY_LATLONG, error=exc)
def __str__(self) -> str: def __str__(self) -> str:
@ -695,22 +683,26 @@ def generate_fingerprinted_user_agent() -> Result[str]:
""" """
version: str = ".".join([str(v) for v in VERSION]) + VERSION_SUFFIX version: str = ".".join([str(v) for v in VERSION]) + VERSION_SUFFIX
try: def _try(func: Callable) -> str:
system_info: str = platform() try:
hostname: str = gethostname() return func()
mac_address: str = ":".join(
[ except Exception: # noqa: BLE001
"{:02x}".format((getnode() >> elements) & 0xFF) return "unknown"
for elements in range(0, 2 * 6, 2)
][::-1] system_info: str = _try(platform)
hostname = _try(gethostname)
mac_address = _try(
lambda: ":".join(
[f"{(getnode() >> elements) & 0xFF:02x}" for elements in range(0, 2 * 6, 2)][::-1]
) )
unique_info: str = f"{version}-{system_info}-{hostname}-{mac_address}" )
unique_info = f"{version}-{system_info}-{hostname}-{mac_address}"
except Exception as exc: if unique_info == "unknown-unknown-unknown-unknown":
return Result[str](f"surplus/{version} (generic-user)", error=exc) return Result[str](f"surplus/{version} (generic-user)")
fingerprint: str = shake_256(unique_info.encode()).hexdigest(5)
fingerprint: str = shake_256(unique_info.encode()).hexdigest(6)
return Result[str](f"surplus/{version} ({fingerprint})") return Result[str](f"surplus/{version} ({fingerprint})")
@ -740,17 +732,14 @@ class SurplusDefaultGeocoding:
""" """
user_agent: str = default_fingerprint user_agent: str = default_fingerprint
_ratelimited_raw_geocoder: Callable | None = None _ratelimited_raw_geocoder: Callable = lambda _: None # noqa: E731
_ratelimited_raw_reverser: Callable | None = None _ratelimited_raw_reverser: Callable = lambda _: None # noqa: E731
_first_update: bool = False _first_update: bool = False
def update_geocoding_functions(self) -> None: def update_geocoding_functions(self) -> None:
""" """
re-initialise the geocoding functions with the current user agent, also generate re-initialise the geocoding functions with the current user agent, also generate
a new user agent if not set properly a new user agent if not set properly
recommended to call this before using surplus as by default the geocoding
functions are uninitialised
""" """
if not isinstance(self.user_agent, str): if not isinstance(self.user_agent, str):
@ -785,18 +774,14 @@ class SurplusDefaultGeocoding:
see SurplusGeocoderProtocol for more information on surplus geocoder functions see SurplusGeocoderProtocol for more information on surplus geocoder functions
""" """
if not callable(self._ratelimited_raw_geocoder) or (self._first_update is False): if self._first_update is False:
self.update_geocoding_functions() self.update_geocoding_functions()
# https://github.com/python/mypy/issues/12155
assert callable(self._ratelimited_raw_geocoder)
location: _geopy_Location | None = self._ratelimited_raw_geocoder(place) location: _geopy_Location | None = self._ratelimited_raw_geocoder(place)
if location is None: if location is None:
raise NoSuitableLocationError( msg = f"No suitable location could be geolocated from '{place}'"
f"No suitable location could be geolocated from '{place}'" raise NoSuitableLocationError(msg)
)
bounding_box: tuple[float, float, float, float] | None = location.raw.get( bounding_box: tuple[float, float, float, float] | None = location.raw.get(
"boundingbox", None "boundingbox", None
@ -804,7 +789,7 @@ class SurplusDefaultGeocoding:
if location.raw.get("boundingbox", None) is not None: if location.raw.get("boundingbox", None) is not None:
_bounding_box = [float(c) for c in location.raw.get("boundingbox", [])] _bounding_box = [float(c) for c in location.raw.get("boundingbox", [])]
if len(_bounding_box) == 4: if len(_bounding_box) == 4: # noqa: PLR2004
bounding_box = ( bounding_box = (
_bounding_box[0], _bounding_box[0],
_bounding_box[1], _bounding_box[1],
@ -830,18 +815,14 @@ class SurplusDefaultGeocoding:
see SurplusReverserProtocol for more information on surplus reverser functions see SurplusReverserProtocol for more information on surplus reverser functions
""" """
if not callable(self._ratelimited_raw_reverser) or (self._first_update is False): if self._first_update is False:
self.update_geocoding_functions() self.update_geocoding_functions()
# https://github.com/python/mypy/issues/12155 location: _geopy_Location | None = self._ratelimited_raw_reverser(str(latlong), zoom=level)
assert callable(self._ratelimited_raw_reverser)
location: _geopy_Location | None = self._ratelimited_raw_reverser(
str(latlong), zoom=level
)
if location is None: if location is None:
raise NoSuitableLocationError(f"could not reverse '{str(latlong)}'") msg = f"could not reverse '{latlong!s}'"
raise NoSuitableLocationError(msg)
location_dict: dict[str, Any] = {} location_dict: dict[str, Any] = {}
@ -855,32 +836,7 @@ class SurplusDefaultGeocoding:
return location_dict return location_dict
default_geocoding: Final[SurplusDefaultGeocoding] = SurplusDefaultGeocoding( default_geocoding: Final[SurplusDefaultGeocoding] = SurplusDefaultGeocoding(default_fingerprint)
default_fingerprint
)
default_geocoding.update_geocoding_functions()
def default_geocoder(place: str) -> Latlong:
"""(deprecated) geocoder for surplus, uses OpenStreetMap Nominatim"""
print(
"warning: default_geocoder is deprecated. "
"this is a emulation function that will use a fingerprinted user agent.",
file=stderr,
)
return default_geocoding.geocoder(place=place)
def default_reverser(latlong: Latlong, level: int = 18) -> dict[str, Any]:
"""
(deprecated) reverser for surplus, uses OpenStreetMap Nominatim
"""
print(
"warning: default_reverser is deprecated. "
"this is a emulation function that will use a fingerprinted user agent.",
file=stderr,
)
return default_geocoding.reverser(latlong=latlong, level=level)
class Behaviour(NamedTuple): class Behaviour(NamedTuple):
@ -909,6 +865,8 @@ class Behaviour(NamedTuple):
what type to convert query to what type to convert query to
using_termux_location: bool = False using_termux_location: bool = False
treats query as a termux-location output json string, and parses it accordingly treats query as a termux-location output json string, and parses it accordingly
show_user_agent: bool = False
whether to print the fingerprinted user agent and exit
""" """
query: str | list[str] = "" query: str | list[str] = ""
@ -920,6 +878,7 @@ class Behaviour(NamedTuple):
version_header: bool = False version_header: bool = False
convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT
using_termux_location: bool = False using_termux_location: bool = False
show_user_agent: bool = False
# functions # functions
@ -960,8 +919,8 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
original_query = str(behaviour.query) original_query = str(behaviour.query)
split_query = behaviour.query.split(" ") split_query = behaviour.query.split(" ")
for word in split_query: for _word in split_query:
word = word.strip(",").strip() word = _word.strip(",").strip()
if validator.is_valid(word): if validator.is_valid(word):
portion_plus_code = word portion_plus_code = word
@ -1023,7 +982,7 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
print(f"debug: parse_query: {behaviour.query=}", file=behaviour.stderr) print(f"debug: parse_query: {behaviour.query=}", file=behaviour.stderr)
# check if empty # check if empty
if (behaviour.query == []) or (behaviour.query == ""): if behaviour.query in ([], ""):
return Result[Query]( return Result[Query](
LatlongQuery(EMPTY_LATLONG), LatlongQuery(EMPTY_LATLONG),
error=EmptyQueryError("empty query string passed"), error=EmptyQueryError("empty query string passed"),
@ -1063,7 +1022,8 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
try: try:
termux_location_json = json_loads(original_query) termux_location_json = json_loads(original_query)
if not isinstance(termux_location_json, dict): if not isinstance(termux_location_json, dict):
raise ValueError("parsed termux-location json is not a dict") msg = "parsed termux-location json is not a dict"
raise TypeError(msg) # noqa: TRY301
return Result[Query]( return Result[Query](
LatlongQuery( LatlongQuery(
@ -1074,13 +1034,13 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
) )
) )
except (JSONDecodeError, TypeError) as exc: except (JSONDecodeError, TypeError):
return Result[Query]( return Result[Query](
LatlongQuery(EMPTY_LATLONG), LatlongQuery(EMPTY_LATLONG),
error=ValueError("could not parse termux-location json"), error=ValueError("could not parse termux-location json"),
) )
except KeyError as exc: except KeyError:
return Result[Query]( return Result[Query](
LatlongQuery(EMPTY_LATLONG), LatlongQuery(EMPTY_LATLONG),
error=ValueError( error=ValueError(
@ -1088,7 +1048,7 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
), ),
) )
except Exception as exc: except Exception as exc: # noqa: BLE001
return Result[Query]( return Result[Query](
LatlongQuery(EMPTY_LATLONG), LatlongQuery(EMPTY_LATLONG),
error=exc, error=exc,
@ -1107,29 +1067,29 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
if "," not in single: # no comma, not a latlong coord if "," not in single: # no comma, not a latlong coord
return Result[Query](StringQuery(original_query)) return Result[Query](StringQuery(original_query))
else: # has comma, possibly a latlong coord # has comma, possibly a latlong coord
comma_split_single: list[str] = single.split(",") comma_split_single: list[str] = single.split(",")
if len(comma_split_single) == 2: if len(comma_split_single) == 2: # noqa: PLR2004
try: # try to type cast query try: # try to type cast query
latitude = float(comma_split_single[0].strip(",")) latitude = float(comma_split_single[0].strip(","))
longitude = float(comma_split_single[-1].strip(",")) longitude = float(comma_split_single[-1].strip(","))
except ValueError: # not a latlong coord, fallback except ValueError: # not a latlong coord, fallback
return Result[Query](StringQuery(single)) return Result[Query](StringQuery(single))
else: # are floats, so is a latlong coord else: # are floats, so is a latlong coord
return Result[Query]( return Result[Query](
LatlongQuery( LatlongQuery(
Latlong( Latlong(
latitude=latitude, latitude=latitude,
longitude=longitude, longitude=longitude,
)
) )
) )
)
# not a latlong coord, fallback # not a latlong coord, fallback
return Result[Query](StringQuery(original_query)) return Result[Query](StringQuery(original_query))
case [left_single, right_single]: case [left_single, right_single]:
# possibly a: # possibly a:
@ -1144,9 +1104,7 @@ def parse_query(behaviour: Behaviour) -> Result[Query]:
return Result[Query](StringQuery(original_query)) return Result[Query](StringQuery(original_query))
else: # are floats, so is a latlong coord else: # are floats, so is a latlong coord
return Result[Query]( return Result[Query](LatlongQuery(Latlong(latitude=latitude, longitude=longitude)))
LatlongQuery(Latlong(latitude=latitude, longitude=longitude))
)
case _: case _:
# possibly a: # possibly a:
@ -1193,24 +1151,35 @@ def handle_args() -> Behaviour:
default=False, default=False,
help="prints version information to stderr and exits", help="prints version information to stderr and exits",
) )
parser.add_argument( (
"-c", parser.add_argument(
"--convert-to", "-c",
type=str, "--convert-to",
choices=[str(v.value) for v in ConversionResultTypeEnum], type=str,
help=( choices=[str(v.value) for v in ConversionResultTypeEnum],
"converts query a specific output type, defaults to " help=(
f"'{Behaviour([]).convert_to_type.value}'" "converts query a specific output type, defaults to "
f"'{Behaviour([]).convert_to_type.value}'"
),
default=Behaviour([]).convert_to_type.value,
), ),
default=Behaviour([]).convert_to_type.value, )
),
parser.add_argument( parser.add_argument(
"-u", "-u",
"--user-agent", "--user-agent",
type=str, type=str,
help=f"user agent string to use for geocoding service, defaults to fingerprinted user agent string", help=(
"user agent string to use for geocoding service, "
"defaults to fingerprinted user agent string"
),
default=default_fingerprint, default=default_fingerprint,
) )
parser.add_argument(
"--show-user-agent",
action="store_true",
default=False,
help="prints fingerprinted user agent string and exits",
)
parser.add_argument( parser.add_argument(
"-t", "-t",
"--using-termux-location", "--using-termux-location",
@ -1224,20 +1193,11 @@ def handle_args() -> Behaviour:
query: str | list[str] = "" query: str | list[str] = ""
# "-" stdin check # "-" stdin check
if args.query == ["-"]: query = "\n".join([line.strip() for line in stdin]) if (args.query == ["-"]) else args.query
stdin_query: list[str] = []
for line in stdin:
stdin_query.append(line.strip())
query = "\n".join(stdin_query)
else:
query = args.query
# setup structures and return # setup structures and return
geocoding = SurplusDefaultGeocoding(args.user_agent) geocoding = SurplusDefaultGeocoding(args.user_agent)
behaviour = Behaviour( return Behaviour(
query=query, query=query,
geocoder=geocoding.geocoder, geocoder=geocoding.geocoder,
reverser=geocoding.reverser, reverser=geocoding.reverser,
@ -1247,14 +1207,14 @@ def handle_args() -> Behaviour:
version_header=args.version, version_header=args.version,
convert_to_type=ConversionResultTypeEnum(args.convert_to), convert_to_type=ConversionResultTypeEnum(args.convert_to),
using_termux_location=args.using_termux_location, using_termux_location=args.using_termux_location,
show_user_agent=args.show_user_agent,
) )
return behaviour
def _unique(l: Sequence[str]) -> list[str]: def _unique(container: Sequence[str]) -> list[str]:
"""(internal function) returns a in-order unique list from list""" """(internal function) returns a in-order unique list from list"""
unique: OrderedDict = OrderedDict() unique: OrderedDict = OrderedDict()
for line in l: for line in container:
unique.update({line: None}) unique.update({line: None})
return list(unique.keys()) return list(unique.keys())
@ -1263,7 +1223,7 @@ def _generate_text(
location: dict[str, Any], location: dict[str, Any],
behaviour: Behaviour, behaviour: Behaviour,
mode: TextGenerationEnum = TextGenerationEnum.SHAREABLE_TEXT, mode: TextGenerationEnum = TextGenerationEnum.SHAREABLE_TEXT,
debug: bool = False, debug: bool = False, # noqa: FBT001, FBT002
) -> str: ) -> str:
""" """
(internal function) generate shareable text from location dict (internal function) generate shareable text from location dict
@ -1286,7 +1246,7 @@ def _generate_text(
line_number: int, line_number: int,
line_keys: Sequence[str], line_keys: Sequence[str],
separator: str = ", ", separator: str = ", ",
filter: Callable[[str], list[bool]] = lambda e: [True], filter_func: Callable[[str], list[bool]] = lambda _: [True],
) -> str: ) -> str:
""" """
(internal function) generate a line of shareable text from a list of keys (internal function) generate a line of shareable text from a list of keys
@ -1298,7 +1258,7 @@ def _generate_text(
list of keys to .get() from location dict list of keys to .get() from location dict
separator: str = ", " separator: str = ", "
separator to join elements with separator to join elements with
filter: Callable[[str], list[bool]] = lambda e: True filter_func: Callable[[str], list[bool]] = lambda e: True
function that takes in a string and returns a list of bools, used to function that takes in a string and returns a list of bools, used to
filter elements from line_keys. list will be passed to all(). if all filter elements from line_keys. list will be passed to all(). if all
returns True, then the element is kept. returns True, then the element is kept.
@ -1313,14 +1273,14 @@ def _generate_text(
if detail == "": if detail == "":
continue continue
# filtering: if all(filter(detail)) returns True, # filtering: if all(filter_func(detail)) returns True,
# then the element is kept/added to 'basket' # then the element is kept/added to 'basket'
if filter_status := all(detail_check := filter(detail)) is True: if filter_status := all(detail_check := filter_func(detail)) is True:
if debug: if debug:
print( print(
"debug: _generate_text_line: " "debug: _generate_text_line: "
f"{str(detail_check):<20} -> {str(filter_status):<5} " f"{detail_check!s:<20} -> {filter_status!s:<5} "
f"-------- '{detail}'", f"-------- '{detail}'",
file=behaviour.stderr, file=behaviour.stderr,
) )
@ -1331,7 +1291,7 @@ def _generate_text(
if debug: if debug:
print( print(
"debug: _generate_text_line: " "debug: _generate_text_line: "
f"{str(detail_check):<20} -> {str(filter_status):<5}" f"{detail_check!s:<20} -> {filter_status!s:<5}"
f" filtered '{detail}'", f" filtered '{detail}'",
file=behaviour.stderr, file=behaviour.stderr,
) )
@ -1358,15 +1318,14 @@ def _generate_text(
C: dict content C: dict content
""" """
DEFAULT = "default" country: str = "default"
country: str = DEFAULT
if len(iso3166_2) >= 1: if len(iso3166_2) >= 1:
country = split_iso3166_2[0] country = split_iso3166_2[0]
if country not in line_keys: if country not in line_keys:
return False, line_keys[DEFAULT] return False, line_keys["default"]
else:
return True, line_keys[country] return True, line_keys[country]
# iso3166-2 handling: this allows surplus to have special key arrangements for a # iso3166-2 handling: this allows surplus to have special key arrangements for a
# specific iso3166-2 code for edge cases # specific iso3166-2 code for edge cases
@ -1464,11 +1423,11 @@ def _generate_text(
# filter: everything here should be True if the element is to be kept # filter: everything here should be True if the element is to be kept
if line_filter is False: if line_filter is False:
_filter = lambda e: [True] _filter = lambda _: [True] # noqa: E731
else: else:
_filter = lambda ak: [ _filter = lambda ak: [ # noqa: E731
ak not in general_global_info, ak not in general_global_info,
not any(True if (ak in sn) else False for sn in seen_names), not any(ak in sn for sn in seen_names),
] ]
text.append( text.append(
@ -1476,7 +1435,7 @@ def _generate_text(
line_number=line_number, line_number=line_number,
line_keys=line_keys, line_keys=line_keys,
separator=line_separator, separator=line_separator,
filter=_filter, filter_func=_filter,
) )
) )
@ -1489,9 +1448,8 @@ def _generate_text(
) )
case _: case _:
raise NotImplementedError( msg = f"unknown mode '{mode}' (expected a TextGenerationEnum)"
f"unknown mode '{mode}' (expected a TextGenerationEnum)" raise NotImplementedError(msg)
)
def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
@ -1506,7 +1464,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
returns Result[str] returns Result[str]
""" """
if not isinstance(query, (PlusCodeQuery, LocalCodeQuery, LatlongQuery, StringQuery)): if not isinstance(query, PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery):
query_result = parse_query( query_result = parse_query(
behaviour=Behaviour( behaviour=Behaviour(
query=str(query), query=str(query),
@ -1531,9 +1489,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
match behaviour.convert_to_type: match behaviour.convert_to_type:
case ConversionResultTypeEnum.SHAREABLE_TEXT: case ConversionResultTypeEnum.SHAREABLE_TEXT:
# get latlong and handle result # get latlong and handle result
latlong_result: Result[Latlong] = query.to_lat_long_coord( latlong_result: Result[Latlong] = query.to_lat_long_coord(geocoder=behaviour.geocoder)
geocoder=behaviour.geocoder
)
if not latlong_result: if not latlong_result:
return Result[str]("", error=latlong_result.error) return Result[str]("", error=latlong_result.error)
@ -1545,7 +1501,7 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
try: try:
location = behaviour.reverser(latlong_result.get()) location = behaviour.reverser(latlong_result.get())
except Exception as exc: except Exception as exc: # noqa: BLE001
return Result[str]("", error=exc) return Result[str]("", error=exc)
if behaviour.debug: if behaviour.debug:
@ -1585,11 +1541,11 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
# perform operation # perform operation
try: try:
pluscode: str = _PlusCode_encode( pluscode: str = _encode(
lat=latlong_query.get().latitude, lon=latlong_query.get().longitude lat=latlong_query.get().latitude, lon=latlong_query.get().longitude
) )
except Exception as exc: except Exception as exc: # noqa: BLE001
return Result[str]("", error=exc) return Result[str]("", error=exc)
return Result[str](pluscode) return Result[str](pluscode)
@ -1612,11 +1568,9 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
# reverse location and handle result # reverse location and handle result
try: try:
location = behaviour.reverser( location = behaviour.reverser(query_latlong, level=LOCALITY_GEOCODER_LEVEL)
query_latlong, level=LOCALITY_GEOCODER_LEVEL
)
except Exception as exc: except Exception as exc: # noqa: BLE001
return Result[str]("", error=exc) return Result[str]("", error=exc)
if behaviour.debug: if behaviour.debug:
@ -1624,13 +1578,14 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
# generate locality portion of local code # generate locality portion of local code
if behaviour.debug: if behaviour.debug:
print( stdout.write(
_generate_text( _generate_text(
location=location, location=location,
behaviour=behaviour, behaviour=behaviour,
mode=TextGenerationEnum.LOCALITY_TEXT, mode=TextGenerationEnum.LOCALITY_TEXT,
debug=behaviour.debug, debug=behaviour.debug,
).strip() ).strip()
+ "\n"
) )
portion_locality: str = _generate_text( portion_locality: str = _generate_text(
@ -1644,25 +1599,31 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
locality_latlong: Latlong = behaviour.geocoder(portion_locality) locality_latlong: Latlong = behaviour.geocoder(portion_locality)
# check now if bounding_box is set and valid # check now if bounding_box is set and valid
assert locality_latlong.bounding_box is not None, ( if getattr(locality_latlong, "bounding_box", None) is None:
"(shortening) geocoder-returned latlong has .bounding_box=None" msg = (
f" - {locality_latlong.bounding_box}" "(shortening) geocoder-returned latlong has .bounding_box=None"
) f" - {locality_latlong.bounding_box}"
)
raise AttributeError(msg) # noqa: TRY301
assert len(locality_latlong.bounding_box) == 4, ( if len(locality_latlong.bounding_box) != 4: # noqa: PLR2004
"(shortening) geocoder-returned latlong has len(.bounding_box) < 4" msg = (
f" - {locality_latlong.bounding_box}" "(shortening) geocoder-returned latlong has len(.bounding_box) != 4"
) f" - {locality_latlong.bounding_box}"
)
raise ValueError(msg) # noqa: TRY301
assert all([type(c) == float for c in locality_latlong.bounding_box]), ( if not all(type(c) == float(c) for c in locality_latlong.bounding_box):
"(shortening) geocoder-returned latlong has non-float in .bounding_box" msg = (
f" - {locality_latlong.bounding_box}" "(shortening) geocoder-returned latlong has non-float in .bounding_box"
) f" - {locality_latlong.bounding_box}"
)
raise TypeError(msg) # noqa: TRY301
except Exception as exc: except Exception as exc: # noqa: BLE001
return Result[str]("", error=exc) return Result[str]("", error=exc)
plus_code = _PlusCode_encode( plus_code = _encode(
lat=query_latlong.latitude, lat=query_latlong.latitude,
lon=query_latlong.longitude, lon=query_latlong.longitude,
) )
@ -1682,10 +1643,8 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
<= (query_latlong.longitude + 0.4) <= (query_latlong.longitude + 0.4)
), ),
# The bounding box of the feature is less than 0.8 degrees high and wide. # The bounding box of the feature is less than 0.8 degrees high and wide.
abs(locality_latlong.bounding_box[0] - locality_latlong.bounding_box[1]) abs(locality_latlong.bounding_box[0] - locality_latlong.bounding_box[1]) < 0.8, # noqa: PLR2004
< 0.8, abs(locality_latlong.bounding_box[2] - locality_latlong.bounding_box[3]) < 0.8, # noqa: PLR2004
abs(locality_latlong.bounding_box[2] - locality_latlong.bounding_box[3])
< 0.8,
) )
check2 = ( check2 = (
@ -1702,16 +1661,14 @@ def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]:
<= (query_latlong.longitude + 8) <= (query_latlong.longitude + 8)
), ),
# The bounding box of the feature is less than 0.8 degrees high and wide. # The bounding box of the feature is less than 0.8 degrees high and wide.
abs(locality_latlong.bounding_box[0] - locality_latlong.bounding_box[1]) abs(locality_latlong.bounding_box[0] - locality_latlong.bounding_box[1]) < 16, # noqa: PLR2004
< 16, abs(locality_latlong.bounding_box[2] - locality_latlong.bounding_box[3]) < 16, # noqa: PLR2004
abs(locality_latlong.bounding_box[2] - locality_latlong.bounding_box[3])
< 16,
) )
if check1: if check1:
return Result[str](f"{plus_code[4:]} {portion_locality}") return Result[str](f"{plus_code[4:]} {portion_locality}")
elif check2: if check2:
return Result[str](f"{plus_code[2:]} {portion_locality}") return Result[str](f"{plus_code[2:]} {portion_locality}")
print( print(
@ -1755,7 +1712,7 @@ def cli() -> int:
# handle arguments and print version header # handle arguments and print version header
print( print(
f"surplus version {'.'.join([str(v) for v in VERSION])}{VERSION_SUFFIX}" f"surplus version {'.'.join([str(v) for v in VERSION])}{VERSION_SUFFIX}"
+ (f", debug mode" if behaviour.debug else "") + (", debug mode" if behaviour.debug else "")
+ ( + (
( (
f" ({BUILD_COMMIT[:10]}@{BUILD_BRANCH}, " f" ({BUILD_COMMIT[:10]}@{BUILD_BRANCH}, "
@ -1768,7 +1725,14 @@ def cli() -> int:
) )
if behaviour.version_header: if behaviour.version_header:
exit(0) sysexit(0)
if behaviour.show_user_agent:
print(
generate_fingerprinted_user_agent().get(),
file=behaviour.stdout,
)
sysexit(0)
# parse query and handle result # parse query and handle result
query = parse_query(behaviour=behaviour) query = parse_query(behaviour=behaviour)
@ -1796,4 +1760,4 @@ def cli() -> int:
if __name__ == "__main__": if __name__ == "__main__":
exit(cli()) sysexit(cli())

View file

@ -0,0 +1,24 @@
"""
script to copy shell scripts into the docs folder for publishing
src/surplus-on-wheels/s+ow -> docs/spow.sh
src/surplus-on-wheels/termux-s+ow-setup -> docs/termux.sh
src/spow-whatsapp-bridge/install.sh -> docs/whatsapp.sh
src/spow-telegram-bridge/install.sh -> docs/telegram.sh
"""
from pathlib import Path
from shutil import copyfile
repo_root: Path = Path(__file__).parent.parent.parent
docs_path: Path = repo_root.joinpath("docs")
copy_map: dict[Path, Path] = {
repo_root.joinpath("src/surplus-on-wheels/s+ow"): docs_path.joinpath("spow.sh"),
repo_root.joinpath("src/surplus-on-wheels/install.sh"): docs_path.joinpath("termux.sh"),
repo_root.joinpath("src/spow-whatsapp-bridge/install.sh"): docs_path.joinpath("whatsapp.sh"),
repo_root.joinpath("src/spow-telegram-bridge/install.sh"): docs_path.joinpath("telegram.sh"),
}
for target, destination in copy_map.items():
copyfile(target, destination)
print(f"{target}\t->\t{destination}")

View file

@ -33,29 +33,37 @@ from datetime import datetime, timedelta, timezone
from os import getenv from os import getenv
from pathlib import Path from pathlib import Path
from subprocess import run from subprocess import run
from sys import exit as sysexit
# NOTE: change this if surplus has moved # NOTE: change this if surplus has moved
path_surplus = Path(__file__).parent.joinpath("./surplus/surplus.py") path_surplus = Path(__file__).parent.joinpath("./src/surplus/surplus.py")
build_time = datetime.now(timezone(timedelta(hours=8))) # using SGT build_time = datetime.now(timezone(timedelta(hours=8))) # using SGT
_insert_build_branch = getenv( _insert_build_branch = getenv(
"SURPLUS_BUILD_BRANCH", "SURPLUS_BUILD_BRANCH",
run( run(
"git branch --show-current", "git branch --show-current".split(),
capture_output=True, capture_output=True,
text=True, text=True,
shell=True, check=False,
).stdout.strip("\n"), )
.stdout.strip("\n")
.strip(),
) )
insert_build_branch = _insert_build_branch if _insert_build_branch != "" else "unknown" insert_build_branch = _insert_build_branch if _insert_build_branch != "" else "unknown"
insert_build_commit: str = run( _insert_build_commit: str = (
"git rev-parse HEAD", run(
capture_output=True, "git rev-parse HEAD".split(),
text=True, capture_output=True,
shell=True, text=True,
).stdout.strip("\n") check=False,
)
.stdout.strip("\n")
.strip()
)
insert_build_commit = _insert_build_commit if _insert_build_commit != "" else "unknown"
insert_build_datetime: str = repr(build_time).replace("datetime.", "") insert_build_datetime: str = repr(build_time).replace("datetime.", "")
@ -63,7 +71,7 @@ insert_build_datetime: str = repr(build_time).replace("datetime.", "")
targets: list[tuple[str, str]] = [ targets: list[tuple[str, str]] = [
( (
'VERSION_SUFFIX: Final[str] = "-local"', 'VERSION_SUFFIX: Final[str] = "-local"',
'VERSION_SUFFIX: Final[str] = ""', 'VERSION_SUFFIX: Final[str] = "-alpha"',
), ),
( (
'BUILD_BRANCH: Final[str] = "future"', 'BUILD_BRANCH: Final[str] = "future"',
@ -81,18 +89,19 @@ targets: list[tuple[str, str]] = [
def main() -> int: def main() -> int:
assert path_surplus.is_file() and path_surplus.exists(), f"{path_surplus} not found" if not (path_surplus.is_file() and path_surplus.exists()):
raise FileNotFoundError(path_surplus)
source_surplus: str = path_surplus.read_text(encoding="utf-8") source_surplus: str = path_surplus.read_text(encoding="utf-8")
for old, new in targets: for old, new in targets:
print(f"new: {new}\nold: {old}\n") print(f" old: {old}\n-> new: {new}\n") # noqa: T201
source_surplus = source_surplus.replace(old, new) source_surplus = source_surplus.replace(old, new)
path_surplus.write_text(source_surplus, encoding="utf-8") # path_surplus.write_text(source_surplus, encoding="utf-8")
return 0 return 0
if __name__ == "__main__": if __name__ == "__main__":
exit(main()) sysexit(main())

273
test.py
View file

@ -1,273 +0,0 @@
"""
surplus test runner
-------------------
by mark <mark@joshwel.co>
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or
distribute this software, either in source code form or as a compiled
binary, for any purpose, commercial or non-commercial, and by any
means.
In jurisdictions that recognize copyright laws, the author or authors
of this software dedicate any and all copyright interest in the
software to the public domain. We make this dedication for the benefit
of the public at large and to the detriment of our heirs and
successors. We intend this dedication to be an overt act of
relinquishment in perpetuity of all present and future rights to this
software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
For more information, please refer to <http://unlicense.org/>
"""
from io import StringIO
from sys import stderr
from textwrap import indent
from traceback import format_exception
from typing import Final, NamedTuple
import surplus
INDENT: Final[int] = 3
MINIMUM_PASS_RATE: Final[float] = 0.7 # because results can be flaky
class ContinuityTest(NamedTuple):
query: str
expected: list[str]
class TestFailure(NamedTuple):
test: ContinuityTest
exception: Exception
output: str
stderr: StringIO
tests: list[ContinuityTest] = [
ContinuityTest(
query="8R3M+F8 Singapore",
expected=[("Wisma Atria\n" "435 Orchard Road\n" "238877\n" "Central, Singapore")],
),
ContinuityTest(
query="9R3J+R9 Singapore",
expected=[
(
"Thomson Plaza\n"
"301 Upper Thomson Road\n"
"Sin Ming, Bishan\n"
"574408\n"
"Central, Singapore"
)
],
),
ContinuityTest(
query="3RQ3+HW3 Pemping, Batam City, Riau Islands, Indonesia",
expected=[
("Batam\n" "Kepulauan Riau, Indonesia"),
("Batam\n" "Sumatera, Kepulauan Riau, Indonesia"),
],
),
ContinuityTest(
query="St Lucia, Queensland, Australia G227+XF",
expected=[
(
"The University of Queensland\n"
"Macquarie Street\n"
"St Lucia, Greater Brisbane\n"
"4072\n"
"Queensland, Australia"
),
(
"The University of Queensland\n"
"Eleanor Schonell Bridge\n"
"St Lucia, Greater Brisbane, Dutton Park\n"
"4072\n"
"Queensland, Australia"
),
(
"The University of Queensland\n"
"Hawken Drive\n"
"St Lucia, Greater Brisbane\n"
"4072\n"
"Queensland, Australia"
),
],
),
ContinuityTest(
query="Ngee Ann Polytechnic, Singapore",
expected=[
(
"Ngee Ann Polytechnic\n"
"535 Clementi Road\n"
"Bukit Timah\n"
"599489\n"
"Northwest, Singapore"
),
(
"Ngee Ann Polytechnic\n"
"535 Clementi Road\n"
"Ewart Park, Bukit Timah\n"
"599489\n"
"Northwest, Singapore"
),
],
),
ContinuityTest(
query="1.3521, 103.8198",
expected=[
(
"MacRitchie Nature Trail\n"
"Central Water Catchment\n"
"574325\n"
"Central, Singapore"
)
],
),
ContinuityTest(
query="8WWJ+4P, Singapore", # a comma!
expected=[
(
"Temasek Polytechnic\n"
"21 Tampines Avenue 1\n"
"Tampines West\n"
"529757\n"
"Northeast, Singapore"
),
(
"Temasek Polytechnic\n"
"21 Tampines Avenue 1\n"
"529757\n"
"Southeast, Singapore"
),
],
),
ContinuityTest(
query="J286+WV San Cesario sul Panaro, Modena, Italy",
expected=[
(
"Via Emilia 1193a\n"
"Unione dei comuni del Sorbara, Sant'Anna\n"
"41018 Modena Emilia-Romagna\n"
"Italia"
),
],
),
ContinuityTest(
query="GQ2G+GX Johor Bahru, Johor, Malaysia",
expected=[
(
"The Mall, Mid Valley Southkey\n"
"Jalan Bakar Batu\n"
"81100 Taman Sentosa Johor Bahru\n"
"Iskandar Malaysia, Johor, Malaysia"
),
],
),
]
class SurplusFailure(Exception):
...
class QueryParseFailure(Exception):
...
class ContinuityFailure(Exception):
...
def main() -> int:
failures: list[TestFailure] = []
for idx, test in enumerate(tests, start=1):
print(f"[{idx}/{len(tests)}] {test.query}")
test_stderr = StringIO()
output: str = ""
behaviour = surplus.Behaviour(test.query, stderr=test_stderr, debug=True)
try:
query = surplus.parse_query(behaviour)
if not query:
raise QueryParseFailure(query.cry())
result = surplus.surplus(query.get(), behaviour)
if not result:
raise SurplusFailure(result.cry())
output = result.get()
if output not in test.expected:
raise ContinuityFailure("did not match any expected outputs")
except Exception as exc:
failures.append(
TestFailure(test=test, exception=exc, output=output, stderr=test_stderr)
)
stderr.write(indent(text="(fail)", prefix=INDENT * " ") + "\n\n")
else:
stderr.write(indent(text="(pass)", prefix=INDENT * " ") + "\n\n")
if len(failures) > 0:
print(f"\n--- failures ---\n")
for fail in failures:
print(
f"[{tests.index(fail.test) + 1}/{len(tests)}] {fail.test.query}\n"
+ (
indent("\n".join(format_exception(fail.exception)), prefix=INDENT * " ")
+ "\n"
)
+ (indent(text="Expected:", prefix=INDENT * " "))
)
for expected_output in fail.test.expected:
print(
indent(text=repr(expected_output), prefix=(2 * INDENT) * " ")
+ "\n"
+ (indent(text=expected_output, prefix=(2 * INDENT) * " ") + "\n")
)
print(
indent(text="Actual:", prefix=INDENT * " ")
+ "\n"
+ (indent(text=repr(fail.output), prefix=(2 * INDENT) * " ") + "\n")
+ (indent(text=fail.output, prefix=(2 * INDENT) * " ") + "\n\n")
+ (indent(text="stderr:", prefix=INDENT * " ") + "\n")
+ (indent(text=fail.stderr.getvalue(), prefix=(2 * INDENT) * " "))
)
passes = len(tests) - len(failures)
pass_rate = passes / len(tests)
print(
f"complete: {passes} passed, {len(failures)} failed "
f"({pass_rate * 100:.0f}%/{MINIMUM_PASS_RATE * 100:.0f}%)"
)
if pass_rate < MINIMUM_PASS_RATE:
print("continuity pass rate is under minimum, test suite failed ;<")
return 1
print("continuity tests passed :>")
return 0
if __name__ == "__main__":
exit(main())