From 5737d2223708ce6ce568bf1c16aac3ef8b8961c2 Mon Sep 17 00:00:00 2001 From: Mark Joshwel Date: Tue, 18 Jun 2024 18:53:38 +0800 Subject: [PATCH] many: monorepo-ise --- .gitignore | 2 + CHANGELOG.md | 8 - CONTRIBUTING.md | 116 -- DEVELOPING.md | 316 ----- README.md | 1076 ++---------------- devbox.json | 10 - devbox.lock | 51 - pyproject.toml | 12 +- releaser.py | 9 +- requirements.txt | 8 - src/spow-telegram-bridge/.gitignore | 51 + src/spow-telegram-bridge/UNLICENCE | 24 + src/spow-telegram-bridge/bridge.py | 226 ++++ src/spow-telegram-bridge/poetry.lock | 244 ++++ src/spow-telegram-bridge/pyproject.toml | 34 + src/spow-whatsapp-bridge/.gitignore | 7 + src/spow-whatsapp-bridge/LICENCE | 374 ++++++ src/spow-whatsapp-bridge/build-termux.sh | 4 + src/spow-whatsapp-bridge/go.mod | 22 + src/spow-whatsapp-bridge/go.sum | 42 + src/spow-whatsapp-bridge/main.go | 385 +++++++ src/surplus-on-wheels/.s+ow-bridges | 2 + src/surplus-on-wheels/README.md | 284 +++++ src/surplus-on-wheels/UNLICENCE | 24 + src/surplus-on-wheels/s+ow | 483 ++++++++ src/surplus-on-wheels/termux-s+ow-setup | 22 + src/surplus-on-wheels/termux-s+ow-setup-cron | 12 + src/surplus/UNLICENCE | 24 + {surplus => src/surplus}/__init__.py | 0 {surplus => src/surplus}/py.typed | 0 {surplus => src/surplus}/surplus.py | 6 +- 31 files changed, 2377 insertions(+), 1501 deletions(-) delete mode 100644 CHANGELOG.md delete mode 100644 CONTRIBUTING.md delete mode 100644 DEVELOPING.md delete mode 100644 devbox.json delete mode 100644 devbox.lock delete mode 100644 requirements.txt create mode 100644 src/spow-telegram-bridge/.gitignore create mode 100644 src/spow-telegram-bridge/UNLICENCE create mode 100644 src/spow-telegram-bridge/bridge.py create mode 100644 src/spow-telegram-bridge/poetry.lock create mode 100644 src/spow-telegram-bridge/pyproject.toml create mode 100644 src/spow-whatsapp-bridge/.gitignore create mode 100644 src/spow-whatsapp-bridge/LICENCE create mode 100644 src/spow-whatsapp-bridge/build-termux.sh create mode 100644 src/spow-whatsapp-bridge/go.mod create mode 100644 src/spow-whatsapp-bridge/go.sum create mode 100644 src/spow-whatsapp-bridge/main.go create mode 100644 src/surplus-on-wheels/.s+ow-bridges create mode 100644 src/surplus-on-wheels/README.md create mode 100644 src/surplus-on-wheels/UNLICENCE create mode 100644 src/surplus-on-wheels/s+ow create mode 100644 src/surplus-on-wheels/termux-s+ow-setup create mode 100644 src/surplus-on-wheels/termux-s+ow-setup-cron create mode 100644 src/surplus/UNLICENCE rename {surplus => src/surplus}/__init__.py (100%) rename {surplus => src/surplus}/py.typed (100%) rename {surplus => src/surplus}/surplus.py (99%) diff --git a/.gitignore b/.gitignore index d129451..a14dec7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +old/* + # cached files __pycache__/ *.py[cod] diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 075b109..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,8 +0,0 @@ -# surplus changelog - -this is temporary and will be used to keep track of breaking changes as 2024.0.0 is developed - -## 2024.0.0 - -- `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` diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 48c1523..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,116 +0,0 @@ -# the contributors guide to surplus - -- [git workflow](#git-workflow) -- [reporting incorrect output](#reporting-incorrect-output) - - [the reporting process](#the-reporting-process) - - [what counts as "incorrect"](#what-counts-as-incorrect) - -also see the [DEVELOPING.md](/DEVELOPING.md) file for more information on the codebase. - -## git workflow - -1. fork the repository and branch off from the `future` branch, - or `main` if `future` is not available -2. make and commit your changes! -3. pull in any changes from upstream, and resolve any conflicts, if any -4. **commit your copyright waiver** (_see below_) -5. submit a pull request (_or mail in a diff_) - -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 nickname): - -```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. -``` - -the command to create an empty commit is `git commit --allow-empty` - -## reporting incorrect output - -> [!NOTE] -> this section is independent of the rest of the contributing section. - -different output from the iOS Shortcuts app is expected, however incorrect output is not. - -### the reporting process - -open an issue in the -[repositories issue tracker](https://github.com/markjoshwel/surplus/issues/new), -and do the following: - -1. ensure that your issue is not an error of incorrect data returned by your reverser - function, which by default is OpenStreetMap Nominatim. - (_don't know what the above means? then you are using the default reverser._) - - also look at the ['what counts as "incorrect"'](#what-counts-as-incorrect) section - before moving on. - -2. include the erroneous query. - (_the Plus Code/local code/latlong coordinate/query string you passed into surplus_) - -3. include output from the terminal with the - [`--debug` flag](/README.md#command-line-usage) passed to the surplus CLI or with - `debug=True` set in function calls. - - > [!NOTE] - > if you are using the surplus API and have passed custom stdout and stderr parameters - > to redirect output, include that instead. - -4. how it should look like instead, with reasoning if the error is not obvious. (e.g., - missing details) - - for reference, see how the following issues were written: - - - [issue #4: "Incorrect format: repeated lines"](https://github.com/markjoshwel/surplus/issues/4) - - [issue #6: "Incorrect format: missing details"](https://github.com/markjoshwel/surplus/issues/6) - - [issue #12: "Incorrect format: State before county"](https://github.com/markjoshwel/surplus/issues/12) - -#### what counts as "incorrect" - -- **example** (correct) - - - iOS Shortcuts Output - - ```text - Plaza Singapura - 68 Orchard Rd - 238839 - Singapore - ``` - - - surplus Output - - ```text - Plaza Singapura - 68 Orchard Road - Museum - 238839 - Central, Singapore - ``` - - this _should not_ be reported as incorrect, as the only difference between the two is - that surplus displays more information. - -other examples that _should not_ be reported are: - -- name of place is incorrect/different - - this may be due to incorrect data from the geocoder function, which is OpenStreetMap - Nominatim by default. in the case of Nominatim, it means that the data on OpenStreetMap - is incorrect. - - (_if so, then consider updating OpenStreetMap to help not just you, but other surplus - and OpenStreetMap users!_) - -**you should report** when the output does not make logical sense, or something similar -wherein the output of surplus is illogical to read or is not correct in the traditional -sense of a correct address. - -see the linked issues in [the reporting process](#the-reporting-process) for examples -of incorrect outputs. diff --git a/DEVELOPING.md b/DEVELOPING.md deleted file mode 100644 index fec5496..0000000 --- a/DEVELOPING.md +++ /dev/null @@ -1,316 +0,0 @@ -# the developers guide to surplus - -- [quickstart](#quickstart) -- [common commands](#common-commands) -- [the technical details of surplus's output](#the-technical-details-of-surpluss-output) - -## quickstart - -prerequisites: - -- [Python >=3.11](https://www.python.org/) -- [Hatch](https://hatch.pypa.io/latest/) - -alternatively, use [devbox](https://get.jetpack.io/devbox) for a hermetic development environment powered by [Nix](https://nixos.org/). - -```text -devbox shell # skip this if you aren't using devbox -hatch shell -``` - -## common commands - -- `hatch fmt` - formats and statically analyses the codebase - -- `hatch run dev:check` - runs mypy and isort to check the codebase - -- `hatch run dev:fix` - runs isort to fix imports - -## the technical details of surplus's output - -> [!NOTE] -> this is a breakdown of surplus's output when converting to shareable text. -> when converting to other output types, output may be different. - -```text -$ s+ --debug 8QJF+RP Singapore -surplus version 2.2.0, debug mode (latest@future, Tue 05 Sep 2023 23:38:59 +0800) -debug: parse_query: behaviour.query=['8QJF+RP', 'Singapore'] -debug: _match_plus_code: portion_plus_code='8QJF+RP', portion_locality='Singapore' -debug: cli: query=Result(value=LocalCodeQuery(code='8QJF+RP', locality='Singapore'), error=None) -debug: latlong_result.get()=Latlong(latitude=1.3320625, longitude=103.7743125) -debug: location={...} -debug: _generate_text: split_iso3166_2=['SG', '03'] -debug: _generate_text: using special key arrangements for 'SG-03' (Singapore) -debug: _generate_text: seen_names=['Ngee Ann Polytechnic', 'Clementi Road'] -debug: _generate_text_line: [True] -> True -------- 'Ngee Ann Polytechnic' -debug: _generate_text_line: [True] -> True -------- '535' -debug: _generate_text_line: [True] -> True -------- 'Clementi Road' -debug: _generate_text_line: [True, True] -> True -------- 'Bukit Timah' -debug: _generate_text_line: [False, True] -> False filtered 'Singapore' -debug: _generate_text_line: [True] -> True -------- '599489' -debug: _generate_text_line: [True] -> True -------- 'Northwest' -debug: _generate_text_line: [True] -> True -------- 'Singapore' -0 Ngee Ann Polytechnic -1 -2 -3 535 Clementi Road -4 Bukit Timah -5 599489 -6 Northwest, Singapore -Ngee Ann Polytechnic -535 Clementi Road -Bukit Timah -599489 -Northwest, Singapore -``` - -variables - -- **variables `behaviour.query`, `split_query` and `original_query`** - - (_`split_query` and `original_query` are only shown if query is a latlong coordinate - or query string_) - - `behaviour.query` is the original query string or a list of strings from space-splitting the original query - string passed to [`parse_query()`](/README.md#def-parse_query) for parsing - - `split_query` is the original query string split by spaces - - `original_query` is a single non-split string - - ```text - $ s+ Temasek Polytechnic - ------------------- - query - - behaviour.query -> ['Temasek', 'Polytechnic'] - split_query -> ['Temasek', 'Polytechnic'] - original_query -> 'Temasek Polytechnic' - ``` - - ```text - >>> surplus("77Q4+7X Austin, Texas, USA", surplus.Behaviour()) - - behaviour.query -> '77Q4+7X Austin, Texas, USA' - split_query -> ['77Q4+7X', 'Austin,', 'Texas,', 'USA'] - original_query -> '77Q4+7X Austin, Texas, USA' - ``` - -- **variables `portion_plus_code` and `portion_locality`** - - (_only shown if the query is a local code, not shown on full-length Plus Codes, - latlong coordinates or string queries_) - - represents the Plus Code and locality portions of a - [shortened Plus Code](https://en.wikipedia.org/wiki/Open_Location_Code#Common_usage_and_shortening) - (_referred to as a "local code" in the codebase_) respectively - -- **variable `query`** - - query is a variable of type [`Result[Query]`](/README.md#query) - - this variable is displayed to show what query type [`parse_query()`](/README.md#def-parse_query) has - recognised, and if there were any errors during query parsing - -- **expression `latlong_result.get()=`** - - (_only shown if the query is a Plus Code_) - - the latitude longitude coordinates derived from the Plus Code - -- **variable `location`** - - the response dictionary from the reverser function passed to - [`surplus()`](/README.md#def-surplus) - - for more information on the reverser function, see - [`SurplusReverserProtocol`](/README.md#surplusreverserprotocol) - -- **variable `split_iso3166_2` and special key arrangements** - - a list of strings containing the split iso3166-2 code (country/subdivision identifier) - - if special key arrangements are available for the code, a line similar to the following - will be shown: - - ```text - debug: _generate_text: using special key arrangements for 'SG-03' (Singapore) - ``` - -- **variable `seen_names`** - - a list of unique important names found in certain Nominatim keys used in final output - lines 0-3 - -- **`_generate_text_line` seen name checks** - - ```text - # filter function boolean list status element - # ============================= ======== ====================== - debug: _generate_text_line: [True] -> True -------- 'Ngee Ann Polytechnic' - debug: _generate_text_line: [False, True] -> False filtered 'Singapore' - ``` - - a check is done on shareable text line 4 keys (`SHAREABLE_TEXT_LINE_4_KEYS` - general - regional location) to reduce repeated elements found in `seen_names` - - reasoning is, if an element on line 4 (general regional location) is the exact same as - a previously seen name, there is no need to include the element - - - **filter function boolean list** - - `_generate_text_line`, an internal function defined inside `_generate_text` can be - passed a filter function as a way to filter out certain elements on a line - - ```python - # the filter used in _generate_text, for line 4's seen name checks - filter=lambda ak: [ - # everything here should be True if the element is to be kept - ak not in general_global_info, - not any(True if (ak in sn) else False for sn in seen_names), - ] - ``` - - `general_global_info` is a list of strings containing elements from line 6. (general - global information) - - - **status** - - what `all(filter(detail))` evaluates to, `filter` being the filter function passed to - `_generate_text_line` and `detail` being the current element - - - **element** - - the current iteration from iterating through a list of strings containing elements - from line 4. (general regional location) - -line breakdown of shareable text output, accompanied by their Nominatim keys: - -```text -0 name of a place -1 building name -2 highway name -3 block/house/building number, house name, road -4 general regional location -5 postal code -6 general global information -``` - -0. **name of a place** - - (_usually important places or landmarks_) - - - examples - - ```text - The University of Queensland - Ngee Ann Polytechnic - Botanic Gardens - ``` - - - nominatim keys - - ```text - emergency, historic, military, natural, landuse, place, railway, man_made, - aerialway, boundary, amenity, aeroway, club, craft, leisure, office, mountain_pass, - shop, tourism, bridge, tunnel, waterway - ``` - -1. **building name** - - - examples - - ```text - Novena Square Office Tower A - Visitor Centre - ``` - - - nominatim keys - - ```text - building - ``` - -2. **highway name** - - - examples - - ```text - Marina Coastal Expressway - Lornie Highway - ``` - - - nominatim keys - - ```text - highway - ``` - -3. **block/house/building number, house name, road** - - - examples - - ```text - 535 Clementi Road - Macquarie Street - Braddell Road - ``` - - - nominatim keys - - ```text - house_number, house_name, road - ``` - -4. **general regional location** - - - examples - - ```text - St Lucia, Greater Brisbane - The Drag, Austin - Toa Payoh Crest - ``` - - - nominatim keys - - ```text - residential, neighbourhood, allotments, quarter, city_district, district, borough, - suburb, subdivision, municipality, city, town, village - ``` - -5. **postal code** - - - examples - - ```text - 310131 - 78705 - 4066 - ``` - - - nominatim key - - ```text - postcode - ``` - -6. **general global information** - - - examples - - ```text - Travis County, Texas, United States - Southeast, Singapore - Queensland, Australia - ``` - - - nominatim keys - - ```text - region, county, state, state_district, country, continent - ``` diff --git a/README.md b/README.md index 6a56097..d1a341c 100644 --- a/README.md +++ b/README.md @@ -1,1008 +1,124 @@ # surplus -surplus is a Python script to convert -[Google Maps Plus Codes](https://maps.google.com/pluscodes/) -to iOS Shortcuts-like shareable text. +surplus (s+) is a Python script to convert [Google Maps Plus Codes](https://maps.google.com/pluscodes/) +to iOS Shortcuts-like shareable text -- [installation](#installation) -- [usage](#usage) - - [command-line usage](#command-line-usage) - - [example api usage](#example-api-usage) -- [developer's guide](/DEVELOPING.md) -- [contributor's guide](/CONTRIBUTING.md) -- [api reference](#api-reference) - - [details on the fingerprinted user agent](#details-on-the-fingerprinted-user-agent) -- [licence](#licence) +- [quickstart](#quickstart) +- [documentation](https://joshwel.co/surplus) + - [the user's guide (command-line)](https://joshwel.co/surplus/using#as-a-command-line-tool) + - [the user's guide (library)](https://joshwel.co/surplus/using#as-a-library) + - [the developer's guide](https://joshwel.co/surplus/developing) + - [api reference](https://joshwel.co/surplus/developing) + - [the contributor's guide](https://joshwel.co/surplus/contributing) + +this repository is also monorepo for the following sibling projects: + +- **surplus on wheels** (s+ow) \ + a pure shell script to get your location using `termux-location`, process it through surplus, and + send it to messaging service or wherever, using "bridges" +- **surplus on wheels: whatsapp bridge** +- **surplus on wheels: telegram bridge** + +## quickstart + +> [!TIP] +> termux users can consider [surplus on wheels](https://joshwel.co/surplus/onwheels), 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 -$ surplus 9R3J+R9 Singapore -surplus version 2.2.0 -Thomson Plaza -301 Upper Thomson Road -Sin Ming, Bishan -574408 +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 ``` -## installation +the types of queries you can pass in are: -> [!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) +- 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` -for most, you can install surplus built from the latest stable release: +or, alternatively pass in `-` to read from stdin -```text -pipx install https://github.com/markjoshwel/surplus/releases/latest/download/surplus-latest-py3-none-any.whl -``` +more documentation is available at , +or alternatively available locally in the [docs/](docs) folder -or directly from the repository using pip: +## licences -```text -pipx install git+https://github.com/markjoshwel/surplus.git@main -``` +- [**surplus**](src/surplus) \ + The Unlicence -**Termux users:** consider [surplus on wheels](https://github.com/markjoshwel/surplus-on-wheels), -a sister project that allows you to run surplus regularly throughout the day and send it -to someone on a messaging platform. + surplus is free and unencumbered software released into the public domain. for more information, + please refer to the [UNLICENCE](src/surplus/UNLICENCE), , or the python + module docstring -surplus is also a public domain dedicated [single python file](surplus/surplus.py), so -feel free to grab that and embed it into your own program as you see fit. + however, the direct dependencies surplus relies on are licenced under different, but still + permissive and open-source licences: -see [licence](#licence) for licensing information. + - **[geopy](https://pypi.org/project/geopy/)** — + Python Geocoding Toolbox \ + MIT Licence -## usage + - **[geographiclib](https://pypi.org/project/geographiclib/)** — + The geodesic routines from GeographicLib \ + MIT Licence -### command-line usage + - **[pluscodes](https://pypi.org/project/pluscodes/)** — + Compute Plus Codes (Open Location Codes) \ + Apache 2.0 -```text -usage: surplus [-h] [-d] [-v] [-c {pluscode,localcode,latlong,sharetext}] - [-u USER_AGENT] [--show-user-agent] [-t] - [query ...] +- [**surplus on wheels**](src/surplus-on-wheels) \ + The Unlicence -Google Maps Plus Code to iOS Shortcuts-like shareable text + surplus on wheels is free and unencumbered software released into the public domain. for more + information, please refer to [UNLICENCE](src/surplus-on-wheels/UNLICENCE) or + -positional arguments: - query full-length Plus Code (6PH58QMF+FX), shortened - Plus Code/'local code' (8QMF+FX Singapore), - latlong (1.3336875, 103.7749375), string query - (e.g., 'Wisma Atria'), or '-' to read from stdin +- [**surplus on wheels: WhatsApp Bridge**](src/spow-whatsapp-bridge) \ + Mozilla Public Licence 2.0 -options: - -h, --help show this help message and exit - -d, --debug prints lat, long and reverser response dict to - stderr - -v, --version prints version information to stderr and exits - -c {pluscode,localcode,latlong,sharetext}, --convert-to {pluscode,localcode,latlong,sharetext} - converts query a specific output type, defaults - to 'sharetext' - -u USER_AGENT, --user-agent USER_AGENT - user agent string to use for geocoding service, - defaults to fingerprinted user agent string - --show-user-agent prints fingerprinted user agent string and exits - -t, --using-termux-location - treats input as a termux-location output json - string, and parses it accordingly -``` + the s+ow WhatsApp Bridge is based off mdtest code from the + [whatsmeow](https://github.com/tulir/whatsmeow) project, which is licenced under the Mozilla + Public Licence 2.0. for more information, see [LICENCE](src/spow-whatsapp-bridge/LICENCE), or + -### example api usage + the direct dependencies s+ow-whatsapp-bridge relies on are licenced under different, but still + permissive and open-source licences: -here are a few examples to get you quickly started using surplus in your own program: + - [**whatsmeow**](https://github.com/tulir/whatsmeow) — + Go library for the WhatsApp web multidevice API \ + Mozilla Public Licence 2.0 -1. let surplus do the heavy lifting +- [**surplus on wheels: Telegram Bridge**](src/spow-telegram-bridge) \ + The Unlicence - ```python - >>> from surplus import surplus, Behaviour - >>> result = surplus("Ngee Ann Polytechnic, Singapore", Behaviour()) - >>> result.get() - 'Ngee Ann Polytechnic\n535 Clementi Road\nBukit Timah\n599489\nNorthwest, Singapore' - ``` + the s+ow Telegram Bridge is free and unencumbered software released into the public domain. for + more information, please refer to the [UNLICENCE](src/spow-telegram-bridge/UNLICENCE), + , or the python module docstring -2. handle queries separately + however, the direct dependencies surplus relies on are licenced under different, but still + permissive and open-source licences: - ```python - >>> import surplus - >>> behaviour = surplus.Behaviour("6PH59R48+WP") - >>> query = surplus.parse_query(behaviour) - >>> result = surplus.surplus(query.get(), behaviour) - >>> result.get() - 'MacRitchie Nature Trail\nCentral Water Catchment\n574325\nCentral, Singapore' - ``` - -3. start from a Query object - - ```python - >>> import surplus - >>> localcode = surplus.LocalCodeQuery(code="8R3M+F8", locality="Singapore") - >>> geocoder = surplus.SurplusDefaultGeocoding().geocoder - >>> pluscode_str = localcode.to_full_plus_code(geocoder=geocoder).get() - >>> pluscode = surplus.PlusCodeQuery(pluscode_str) - >>> result = surplus.surplus(pluscode, surplus.Behaviour()) - >>> result.get() - 'Wisma Atria\n435 Orchard Road\n238877\nCentral, Singapore' - ``` - -notes: - -- you can change what surplus does when passing in a custom [`Behaviour`](#class-behaviour) - object - -- most surplus functions return a [`Result`](#class-result) object. while you can - call [`.get()`](#resultget) to obtain the proper return value, this is dangerous and - might raise an exception - -see the [api reference](#api-reference) for more information. - -## api reference - -- [constants](#constants) -- [exception classes](#exception-classes) -- [types](#types) - - [`Query`](#query) - - [`ResultType`](#resulttype) - - [`SurplusGeocoderProtocol`](#surplusgeocoderprotocol) - - [`SurplusReverserProtocol`](#surplusreverserprotocol) -- [`class Behaviour`](#class-behaviour) -- [`class SurplusDefaultGeocoding`](#class-surplusdefaultgeocoding) - - [`SurplusDefaultGeocoding.update_geocoding_functions()`](#surplusdefaultgeocodingupdate_geocoding_functions) - - [`SurplusDefaultGeocoding.geocoder()`](#surplusdefaultgeocodinggeocoder) - - [`SurplusDefaultGeocoding.reverser()`](#surplusdefaultgeocodingreverser) -- [`class ConversionResultTypeEnum`](#class-conversionresulttypeenum) -- [`class Result`](#class-result) - - [`Result.__bool__()`](#result__bool__) - - [`Result.cry()`](#resultcry) - - [`Result.get()`](#resultget) -- [`class Latlong`](#class-latlong) - - [`Latlong.__str__()`](#latlong__str__) -- [`class PlusCodeQuery`](#class-pluscodequery) - - [`PlusCodeQuery.to_lat_long_coord()`](#pluscodequeryto_lat_long_coord) - - [`PlusCodeQuery.__str__()`](#pluscodequery__str__) -- [`class LocalCodeQuery`](#class-localcodequery) - - [`LocalCodeQuery.to_full_plus_code()`](#localcodequeryto_full_plus_code) - - [`LocalCodeQuery.to_lat_long_coord()`](#localcodequeryto_lat_long_coord) - - [`LocalCodeQuery.__str__()`](#localcodequery__str__) -- [`class LatlongQuery`](#class-latlongquery) - - [`LatlongQuery.to_lat_long_coord()`](#latlongqueryto_lat_long_coord) - - [`LatlongQuery.__str__()`](#latlongquery__str__) -- [`class StringQuery`](#class-stringquery) - - [`StringQuery.to_lat_long_coord()`](#stringqueryto_lat_long_coord) - - [`StringQuery.__str__()`](#stringquery__str__) -- [`def surplus()`](#def-surplus) -- [`def parse_query()`](#def-parse_query) -- [`def generate_fingerprinted_user_agent`](#def-generate_fingerprinted_user_agent) - - [details on the fingerprinted user agent](#details-on-the-fingerprinted-user-agent) - -### constants - -- `VERSION: tuple[int, int, int]` - - a tuple of integers representing the version of surplus, in the format - `[major, minor, patch]` - -- `VERSION_SUFFIX: typing.Final[str]` - `BUILD_BRANCH: typing.Final[str]` - `BUILD_COMMIT: typing.Final[str]` - `BUILD_DATETIME: typing.Final[datetime]` - - string and a [datetime.datetime](https://docs.python.org/3/library/datetime.html) object - containing version and build information, set by [releaser.py](releaser.py) - -- `CONNECTION_MAX_RETRIES: int = 9` - `CONNECTION_WAIT_SECONDS: int = 10` - - defines if and how many times to retry a connection, alongside how many seconds to wait - in between tries, for Nominatim - - > [!NOTE] - > this constant only affects the default surplus Nominatim geocoding functions. custom - > functions do not read from this, unless deliberately programmed to do so - -- `SHAREABLE_TEXT_LINE_0_KEYS: dict[str, tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_1_KEYS: dict[str, tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_2_KEYS: dict[str, tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_3_KEYS: dict[str, tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_4_KEYS: dict[str, tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_5_KEYS: dict[str, tuple[str, ...]]` - `SHAREABLE_TEXT_LINE_6_KEYS: dict[str, tuple[str, ...]]` - - a dictionary of iso3166-2 country-portion string keys with a tuple of Nominatim keys - used in shareable text line 0-6 as their values - - ```python - { - "default": (...), - "SG": (...,), - ... - } - ``` - -- `SHAREABLE_TEXT_LINE_SETTINGS: dict[str, dict[int, tuple[str, bool]]]` - - a dictionary of iso3166-2 country-portion string keys with a dictionary as their values - - the dictionary values are dictionaries with integers as keys, and a tuple of two strings - - the first string is the separator string to use, and the second string is a boolean flag - that if `True` will check the line for seen names - - ```python - { - "default": { - 0: (", ", False), - ... - 6: (", ", False), - }, - "IT": { - 0: (", ", False), - ... - 6: (", ", False), - }, - ... - } - ``` - -- `SHAREABLE_TEXT_NAMES: dict[str, tuple[str, ...]]` - - a dictionary of iso3166-2 country-portion string keys with a tuple of strings as their - values - a tuple of strings containing Nominatim keys used in shareable text line 0-2 and - special keys in line 3 - - used for seen name checks - -- `SHAREABLE_TEXT_LOCALITY: dict[str, tuple[str, ...]]` - - a dictionary of iso3166-2 country-portion string keys with a tuple of strings as their - values - - used when generating the locality portions of shortened Plus Codes/local codes - - ```python - { - "default": (...), - "SG": (...,), - ... - } - ``` - -- `SHAREABLE_TEXT_DEFAULT: typing.Final[str]` - constant for what is the "default" key in the `SHAREABLE*` constants - -- `EMPTY_LATLONG: typing.Final[Latlong]` - a constant for an empty latlong coordinate, with latitude and longitude set to 0.0 - -### exception classes - -- `class SurplusError(Exception)` - base skeleton exception for handling and typing surplus exception classes -- `class NoSuitableLocationError(SurplusError)` -- `class IncompletePlusCodeError(SurplusError)` -- `class PlusCodeNotFoundError(SurplusError)` -- `class LatlongParseError(SurplusError)` -- `class EmptyQueryError(SurplusError)` -- `class UnavailableFeatureError(SurplusError)` - -### types - -#### `Query` - -```python -Query: typing.TypeAlias = PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery -``` - -[type alias](https://docs.python.org/3/library/typing.html#type-aliases) representing -either a -[`PlusCodeQuery`](#class-pluscodequery), -[`LocalCodeQuery`](#class-localcodequery), -[`LatlongQuery`](#class-latlongquery) or -[`StringQuery`](#class-stringquery) - -#### `ResultType` - -```python -ResultType = TypeVar("ResultType") -``` - -[generic type](https://docs.python.org/3/library/typing.html#generics) used by -[`Result`](#class-result) - -#### `SurplusGeocoderProtocol` - -[typing_extensions.Protocol](https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols) -class for documentation and static type checking of surplus geocoder functions - -- **signature and conforming function signature** - - ```python - class SurplusGeocoderProtocol(Protocol): - def __call__(self, place: str) -> Latlong: - ... - ``` - - functions that conform to this protocol should have the following signature: - - ```python - def example(place: str) -> Latlong: ... - ``` - -- **information on conforming functions** - - function takes in a location name as a string, and returns a [Latlong](#class-latlong). - - **function MUST supply a `bounding_box` attribute to the to-be-returned - [Latlong](#class-latlong).** the bounding box is used when surplus shortens Plus Codes. - - function can and should be at minimum - [`functools.lru_cache()`-wrapped](https://docs.python.org/3/library/functools.html#functools.lru_cache) - if the geocoding service asks for caching - - exceptions are handled by the caller - -#### `SurplusReverserProtocol` - -[typing_extensions.Protocol](https://mypy.readthedocs.io/en/stable/protocols.html#callback-protocols) -class for documentation and static type checking of surplus reverser functions - -- **signature and conforming function signature** - - ```python - class SurplusReverserProtocol(Protocol): - def __call__(self, latlong: Latlong, level: int = 18) -> dict[str, Any]: - ... - ``` - - functions that conform to this protocol should have the following signature: - - ```python - def example(latlong: Latlong, level: int = 18) -> dict[str, Any]: ... - ``` - -- **information on conforming functions** - - function takes in a [Latlong](#class-latlong) object and return a dictionary with [`SHAREABLE_TEXT_LINE_*_KEYS`](#constants) keys at the dictionaries' top-level. - keys are used to access address information. - - function should also take in an int representing the level of detail for the returned - address, 0-18 (country-level to building), inclusive. should default to 18. - - keys for latitude, longitude and an iso3166-2 (or closest equivalent) should also be - included at the dictionaries top level as the keys `latitude`, `longitude` and - `ISO3166-2` (non-case sensitive, or at least something starting with `ISO3166`) - respectively. - - ```python - { - 'ISO3166-2-lvl6': 'SG-03', - 'amenity': 'Ngee Ann Polytechnic', - ... - 'country': 'Singapore', - 'latitude': 1.33318835, - 'longitude': 103.77461234638255, - 'postcode': '599489', - 'raw': {...}, - } - ``` - - function can and should be at minimum - [`functools.lru_cache()`-wrapped](https://docs.python.org/3/library/functools.html#functools.lru_cache) - if the geocoding service asks for caching - - exceptions are handled by the caller - -### `class Behaviour` - -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) -representing how surplus operations should behave - -attributes - -- `query: str | list[str] = ""` - original user-passed query string or a list of strings from splitting user-passed query - string by spaces - -- `geocoder: SurplusGeocoderProtocol = default_geocoding.geocoder` - name string to location function, see - [`SurplusGeocoderProtocol`](#surplusgeocoderprotocol) for more information - -- `reverser: SurplusReverserProtocol = default_geocoding.reverser` - Latlong object to address information dictionary function, see - [`SurplusReverserProtocol`](#surplusreverserprotocol) for more information - -- `stderr: typing.TextIO = sys.stderr` - [TextIO-like object](https://docs.python.org/3/library/io.html#text-i-o) - representing a writeable file. - defaults to [`sys.stderr`](https://docs.python.org/3/library/sys.html#sys.stderr). - -- `stdout: typing.TextIO = sys.stdout` - [TextIO-like object](https://docs.python.org/3/library/io.html#text-i-o) - representing a writeable file. - defaults to [`sys.stdout`](https://docs.python.org/3/library/sys.html#sys.stdout). - -- `debug: bool = False` - whether to print debug information to stderr - -- `version_header: bool = False` - whether to print version information and exit - -- `convert_to_type: ConversionResultTypeEnum = ConversionResultTypeEnum.SHAREABLE_TEXT` - what type to convert the query to - -- `using_termux_location: bool = False` - treats query as a termux-location output json string, and parses it accordingly - -- `show_user_agent: bool = False` - whether to print the user agent string to stderr - -### `class SurplusDefaultGeocoding` - -> [!IMPORTANT] -> this has replaced the now deprecated default geocoding functions, `default_geocoder()` -> and `default_reverser()`, in surplus 2.1 and later. - -see [SurplusGeocoderProtocol](#surplusgeocoderprotocol) and -[SurplusReverserProtocol](#surplusreverserprotocol) for more information how to -implement a compliant custom geocoder functions. - -[`dataclasses.dataclass`](https://docs.python.org/3/library/dataclasses.html) providing -the default geocoding functionality for surplus, via -[OpenStreetMap Nominatim](https://nominatim.openstreetmap.org/) - -attributes - -- `user_agent: str = default_fingerprint` - pass in a custom user agent here, else it will be the default - [fingerprinted user agent](#details-on-the-fingerprinted-user-agent) - -example usage - -```python -from surplus import surplus, Behaviour, SurplusDefaultGeocoding - -geocoding = SurplusDefaultGeocoding("custom user agent") -geocoding.update_geocoding_functions() # not necessary but recommended - -behaviour = Behaviour( - ..., - geocoder=geocoding.geocoder, - reverser=geocoding.reverser -) - -result = surplus("query", behaviour=behaviour) - -... -``` - -methods - -- [`def update_geocoding_functions(self) -> None: ...`](#surplusdefaultgeocodingupdate_geocoding_functions) -- [`def geocoder(self, place: str) -> Latlong: ...`](#surplusdefaultgeocodinggeocoder) -- [`def reverser(self, latlong: Latlong, level: int = 18) -> dict[str, Any]: ...`](#surplusdefaultgeocodingreverser) - -#### `SurplusDefaultGeocoding.update_geocoding_functions()` - -re-initialise the geocoding functions with the current user agent, also generate a new -user agent if not set properly - -it is recommended to call this before using surplus as by default the geocoding functions -are uninitialised - -- signature - - ```python - def update_geocoding_functions(self) -> None: ... - ``` - -#### `SurplusDefaultGeocoding.geocoder()` - -> [!WARNING] -> this function is primarily given to be passed into a [`Behaviour`](#class-behaviour) -> object, and is not meant to be called directly. - -default geocoder for surplus - -see [SurplusGeocoderProtocol](#surplusgeocoderprotocol) for more information on surplus -geocoder functions - -#### `SurplusDefaultGeocoding.reverser()` - -> [!WARNING] -> this function is primarily given to be passed into a [`Behaviour`](#class-behaviour) -> object, and is not meant to be called directly. - -default reverser for surplus - -see [SurplusReverserProtocol](#surplusreverserprotocol) for more information on surplus -reverser functions - -### `class ConversionResultTypeEnum` - -[enum.Enum](https://docs.python.org/3/library/enum.html) -representing what the result type of conversion should be - -values - -- `PLUS_CODE: str = "pluscode"` -- `LOCAL_CODE: str = "localcode"` -- `LATLONG: str = "latlong"` -- `SHAREABLE_TEXT: str = "sharetext"` - -### `class Result` - -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) -representing the result for safe value retrieval - -attributes - -- `value: ResultType` - value to return or fallback value if erroneous - -- `error: BaseException | None = None` - exception if any - -example usage - -```python -# do something -def some_operation(path) -> Result[str]: - try: - file = open(path) - contents = file.read() - - except Exception as exc: - # must pass a default value - return Result[str]("", error=exc) - - else: - return Result[str](contents) - -# call function and handle result -result = some_operation("some_file.txt") - -if not result: # check if the result is erroneous - # .cry() raises the exception - # (or returns it as a string error message using string=True) - result.cry() - ... - -else: - # .get() raises exception or returns value, - # but since we checked for errors this is safe - print(result.get()) -``` - -methods - -- [`def __bool__(self) -> bool: ...`](#result__bool__) -- [`def cry(self, string: bool = False) -> str: ...`](#resultcry) -- [`def get(self) -> ResultType: ...`](#resultget) - -#### `Result.__bool__()` - -method that returns `True` if `self.error` is not `None` - -- signature - - ```python - def __bool__(self) -> bool: ... - ``` - -- returns `bool` - -#### `Result.cry()` - -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 `self.error` is None - -- signature - - ```python - def cry(self, string: bool = False) -> str: ... - ``` - -- arguments - - - `string: bool = False` - if `self.error` is an Exception, returns it as a string error message - -- returns `str` - -#### `Result.get()` - -method that returns `self.value` if Result is non-erroneous else raises error - -- signature - - ```python - def get(self) -> ResultType: ... - ``` - -- returns `self.value` - -### `class Latlong` - -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) -representing a latitude-longitude coordinate pair - -attributes - -- `latitude: float` -- `longitude: float` -- `bounding_box: tuple[float, float, float, float] | None = None` - a four-tuple representing a bounding box, `(lat1, lat2, lon1, lon2)` or None. - - the user does not need to enter this. the attribute is only used when shortening plus - codes, and would be supplied by the geocoding service during shortening. - -methods - -- [`def __str__(self) -> str: ...`](#latlong__str__) - -#### `Latlong.__str__()` - -method that returns a comma-and-space-separated string of `self.latitude` and -`self.longitude` - -- signature - - ```python - def __str__(self) -> str: ... - ``` - -- returns `str` - -### `class PlusCodeQuery` - -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) -representing a full-length Plus Code (e.g., 6PH58QMF+FX) - -attributes - -- `code: str` - -methods - -- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#pluscodequeryto_lat_long_coord) -- [`def __str__(self) -> str: ...`](#pluscodequery__str__) - -#### `PlusCodeQuery.to_lat_long_coord()` - -- signature - - ```python - def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: - ... - ``` - -- arguments - - - `geocoder: SurplusGeocoderProtocol` - name string to location function, see - [SurplusGeocoderProtocol](#surplusgeocoderprotocol) for more information - -- returns [`Result[Latlong]`](#class-latlong) - -#### `PlusCodeQuery.__str__()` - -method that returns string representation of query - -- signature - - ```python - def __str__(self) -> str: ... - ``` - -- returns `str` - -### `class LocalCodeQuery` - -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) -representing a -[shortened Plus Code](https://en.wikipedia.org/wiki/Open_Location_Code#Common_usage_and_shortening) -with locality, referred to by surplus as a "local code" - -attributes - -- `code: str` - Plus Code portion of local code, e.g., "8QMF+FX" - -- `locality: str` - remaining string of local code, e.g., "Singapore" - -methods - -- [`def to_full_plus_code(self, ...) -> Result[str]: ...`](#localcodequeryto_full_plus_code) -- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#localcodequeryto_lat_long_coord) -- [`def __str__(self) -> str: ...`](#localcodequery__str__) - -#### `LocalCodeQuery.to_full_plus_code()` - -exclusive method that returns a full-length Plus Code as a string - -- signature - - ```python - def to_full_plus_code(self, geocoder: SurplusGeocoderProtocol) -> Result[str]: - ... - ``` - -- arguments - - - `geocoder: SurplusGeocoderProtocol` - name string to location function, see - [SurplusGeocoderProtocol](#surplusgeocoderprotocol) for more information - -- returns [`Result`](#class-result)`[str]` - -#### `LocalCodeQuery.to_lat_long_coord()` - -method that returns a latitude-longitude coordinate pair - -- signature - - ```python - def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: - ... - ``` - -- arguments - - - `geocoder: SurplusGeocoderProtocol` - name string to location function, see - [SurplusGeocoderProtocol](#surplusgeocoderprotocol) for more information - -- returns [`Result[Latlong]`](#class-latlong) - -#### `LocalCodeQuery.__str__()` - -method that returns string representation of query - -- signature - - ```python - def __str__(self) -> str: ... - ``` - -- returns `str` - -### `class LatlongQuery` - -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) -representing a latitude-longitude coordinate pair - -attributes - -- `latlong: Latlong` - -methods - -- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#latlongqueryto_lat_long_coord) -- [`def __str__(self) -> str: ...`](#latlongquery__str__) - -#### `LatlongQuery.to_lat_long_coord()` - -method that returns a latitude-longitude coordinate pair - -- signature - - ```python - def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: - ... - ``` - -- arguments - - - `geocoder: SurplusGeocoderProtocol` - name string to location function, see - [SurplusGeocoderProtocol](#surplusgeocoderprotocol) for more information - -- returns [`Result[Latlong]`](#class-latlong) - -#### `LatlongQuery.__str__()` - -method that returns string representation of query - -- signature - - ```python - def __str__(self) -> str: ... - ``` - -- returns `str` - -### `class StringQuery` - -[`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) -representing a pure string query - -attributes - -- `query: str` - -methods - -- [`def to_lat_long_coord(self, ...) -> Result[Latlong]: ...`](#stringqueryto_lat_long_coord) -- [`def __str__(self) -> str: ...`](#stringquery__str__) - -#### `StringQuery.to_lat_long_coord()` - -method that returns a latitude-longitude coordinate pair - -- signature - - ```python - def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]: - ... - ``` - -- arguments - - - `geocoder: SurplusGeocoderProtocol` - name string to location function, see - [SurplusGeocoderProtocol](#surplusgeocoderprotocol) for more information - -- returns [`Result[Latlong]`](#class-latlong) - -#### `StringQuery.__str__()` - -method that returns string representation of query - -- signature - - ```python - def __str__(self) -> str: ... - ``` - -- returns `str` - -### `def surplus()` - -query to shareable text conversion function - -- signature - - ```python - def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: .. - ``` - -- arguments - - - `query: str | Query` - [query object](#query) to convert or string to attempt to query for then convert - - - `behaviour: Behaviour` - [surplus behaviour namedtuple](#class-behaviour) - -- returns [`Result`](#class-result)`[str]` - -### `def parse_query()` - -function that parses a query string into a query object - -- signature - - ```python - def parse_query(behaviour: Behaviour) -> Result[Query]: ... - ``` - -- arguments - - - `behaviour: Behaviour` - [surplus behaviour namedtuple](#class-behaviour) - -- returns [`Result[Query]`](#query) - -### `def generate_fingerprinted_user_agent()` - -function that attempts to return a unique user agent string. - -- signature - -```python -def generate_fingerprinted_user_agent() -> Result[str]: -``` - -- returns [`Result[str]`](#class-result) - - this result will always have a valid value as erroneous results will have a - resulting value of `'surplus/ (generic-user)'` - - valid results will have a value of `'surplus/ ()'`, where - the fingerprint hash is a 12 character hexadecimal string - -#### details on the fingerprinted user agent - -**why do this in the first place?** -if too many people use surplus at the same time, -Nominatim will start to think it's just one person being greedy. so to prevent this, -surplus will try to generate a unique user agent string for each user through -fingerprinting. - -at the time of writing, the pre-hashed fingerprint string is as follows: - -```python -unique_info: str = f"{version}-{system_info}-{hostname}-{mac_address}" -``` - -it contains the following, in order, alongside an example: - -1. `version` - the surplus version alongside a suffix, if any - - ```text - 2.2.0-local - ``` - -2. `system_info` - generic machine and operating system information - - ```text - Linux-6.5.0-locietta-WSL2-xanmod1-x86_64-with-glibc2.35 - ``` - -3. `hostname` - your computer's hostname - - ```text - mark - ``` - -4. `mac_address` - your computer's mac address - - ```text - A9:36:3C:98:79:33 - ``` - -after hashing, this string becomes a 12 character hexadecimal string, as shown below: - -```text -surplus/2024.0.0 (1fdbfa0b0cfb) - ^^^^^^^^^^^^ - this is the hashed result of unique_info -``` - -if at any time the retrieval of any of these four elements fail, surplus will just give -up and default to `'surplus/ (generic-user)'`. - -you can see the fingerprinted user agent string by running the following command: - -```text -$ surplus --show-user-agent -... -``` - -if any of this seems weird to you, that's fine. pass in a custom user agent flag to -surplus with `-u` or `--user-agent` to override the default user agent, or override the -default user agent in your own code by passing in a custom user agent string to -[`Behaviour`](#class-behaviour). - -```text -$ surplus --user_agent "a-shiny-custom-and-unique-user-agent" 77Q4+7X Austin, Texas, USA -... -``` - -```python ->>> from surplus import surplus, Behaviour ->>> surplus(..., Behaviour(user_agent="a-shiny-custom-and-unique-user-agent")) -... -``` - -## licence - -surplus is free and unencumbered software released into the public domain. for more -information, please refer to the [UNLICENCE](/UNLICENCE), , or the -python module docstring. - -however, direct dependencies of surplus are licensed 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 + - **[Telethon](https://github.com/LonamiWebs/Telethon)** — + Pure Python 3 MTProto API Telegram client library, for bots too! \ + MIT Licence diff --git a/devbox.json b/devbox.json deleted file mode 100644 index 0605e2a..0000000 --- a/devbox.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "packages": [ - "python@3.11.8", - "hatch@latest", - "ruff@latest" - ], - "nixpkgs": { - "commit": "f80ac848e3d6f0c12c52758c0f25c10c97ca3b62" - } -} diff --git a/devbox.lock b/devbox.lock deleted file mode 100644 index fd4072e..0000000 --- a/devbox.lock +++ /dev/null @@ -1,51 +0,0 @@ -{ - "lockfile_version": "1", - "packages": { - "hatch@latest": { - "last_modified": "2024-03-19T05:49:19Z", - "resolved": "github:NixOS/nixpkgs/5710127d9693421e78cca4f74fac2db6d67162b1#hatch", - "source": "devbox-search", - "version": "1.9.0", - "systems": { - "x86_64-linux": { - "store_path": "/nix/store/nxmypvfw8wxjnrx2ngq3dwd2pj2i07q7-hatch-1.9.0" - } - } - }, - "python@3.11.8": { - "last_modified": "2024-03-22T11:26:23Z", - "plugin_version": "0.0.3", - "resolved": "github:NixOS/nixpkgs/a3ed7406349a9335cb4c2a71369b697cecd9d351#python3", - "source": "devbox-search", - "version": "3.11.8", - "systems": { - "aarch64-darwin": { - "store_path": "/nix/store/c05vbvkjxarxkws9zkwrcwrzlsx9nd68-python3-3.11.8" - }, - "aarch64-linux": { - "store_path": "/nix/store/pxzzyri1wbq7kc7pain665g94afkl4ww-python3-3.11.8" - }, - "x86_64-darwin": { - "store_path": "/nix/store/1zaap1xxxvw2ypsgh1mfxb3wzdd49873-python3-3.11.8" - }, - "x86_64-linux": { - "store_path": "/nix/store/7wz6hm9i8wljz0hgwz1wqmn2zlbgavrq-python3-3.11.8" - } - } - }, - "ruff@latest": { - "last_modified": "2024-03-22T11:26:23Z", - "resolved": "github:NixOS/nixpkgs/a3ed7406349a9335cb4c2a71369b697cecd9d351#ruff", - "source": "devbox-search", - "version": "0.3.2", - "systems": { - "aarch64-darwin": { - "store_path": "/nix/store/s3xi1kwxzq7q33pbx8af908724mis2m2-ruff-0.3.2" - }, - "x86_64-darwin": { - "store_path": "/nix/store/f8327vcf7gr5jhj8rh2rp9cadm15ac37-ruff-0.3.2" - } - } - } - } -} diff --git a/pyproject.toml b/pyproject.toml index bebf8af..13e135a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,16 +34,18 @@ surplus = "surplus:cli" exclude = [ "/.github", "/.devbox", + "/src/surplus-on-wheels", + "/src/spow*", ] [tool.hatch.build.targets.wheel] -packages = ["surplus"] +packages = ["src.surplus"] [project.urls] -Documentation = "https://github.com/markjoshwel/surplus#readme" -Issues = "https://github.com/markjoshwel/surplus/issues" +Documentation = "https://joshwel.co/surplus" +Issues = "https://joshwel.co/surplus/issues" Source = "https://github.com/markjoshwel/surplus" -Changelog = "https://github.com/markjoshwel/surplus/releases" +Changelog = "https://joshwel.co/surplus/changelog" [tool.ruff] line-length = 100 @@ -54,7 +56,7 @@ line_length = 100 profile = "black" [tool.hatch.version] -path = "surplus/surplus.py" +path = "src/surplus/surplus.py" [[tool.hatch.envs.all.matrix]] python = ["3.11", "3.12"] diff --git a/releaser.py b/releaser.py index 7d8ef16..b323248 100644 --- a/releaser.py +++ b/releaser.py @@ -36,7 +36,7 @@ from subprocess import run from sys import exit as sysexit # 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 @@ -47,16 +47,17 @@ _insert_build_branch = getenv( capture_output=True, text=True, check=False, - ).stdout.strip("\n"), + ).stdout.strip("\n").strip(), ) insert_build_branch = _insert_build_branch if _insert_build_branch != "" else "unknown" -insert_build_commit: str = run( +_insert_build_commit: str = run( "git rev-parse HEAD".split(), capture_output=True, text=True, check=False, -).stdout.strip("\n") +).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.", "") diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index f058c34..0000000 --- a/requirements.txt +++ /dev/null @@ -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 diff --git a/src/spow-telegram-bridge/.gitignore b/src/spow-telegram-bridge/.gitignore new file mode 100644 index 0000000..11c8e7c --- /dev/null +++ b/src/spow-telegram-bridge/.gitignore @@ -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 \ No newline at end of file diff --git a/src/spow-telegram-bridge/UNLICENCE b/src/spow-telegram-bridge/UNLICENCE new file mode 100644 index 0000000..00d2e13 --- /dev/null +++ b/src/spow-telegram-bridge/UNLICENCE @@ -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 \ No newline at end of file diff --git a/src/spow-telegram-bridge/bridge.py b/src/spow-telegram-bridge/bridge.py new file mode 100644 index 0000000..e344abe --- /dev/null +++ b/src/spow-telegram-bridge/bridge.py @@ -0,0 +1,226 @@ +#!/usr/bin/env python3 +""" +s+ow-telegram-bridge: add-on bridge for surplus on wheels (s+ow) to telegram +---------------------------------------------------------------------------- +by mark + +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 +""" + +import asyncio +from datetime import datetime +from os import environ, chdir +from pathlib import Path +from sys import argv, stderr, stdin +from traceback import print_tb +from typing import Generic, NamedTuple, TypeVar + +from telethon import TelegramClient # type: ignore +from telethon.tl import functions # 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 + + +dir_data: Path = Path.home().joinpath(".local/share/s+ow-telegram-bridge") +dir_data.mkdir(parents=True, exist_ok=True) +chdir(dir_data) +dir_cache: Path = Path.home().joinpath(".cache/s+ow-telegram-bridge") +dir_cache.mkdir(parents=True, exist_ok=True) + + +session = "s+ow-telegram-bridge" +api_id = environ.get("SPOW_TELEGRAM_API_ID", None) +api_hash = environ.get("SPOW_TELEGRAM_API_HASH", None) +message = Path.home().joinpath(".cache/s+ow/message") + + +def handle_error( + exc: Exception | None = None, + 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: {message}{exc_details}", + file=stderr, + ) + + except Exception as exc: + 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) + + if not (message.exists() and message.is_file()): + print("s+ow-telegram-bridge: error: ~/.cache/s+ow/message not found", file=stderr) + exit(1) + + +async def run() -> None: + validate_vars() + 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] = [] + for line in stdin: + for _target in line.split(","): + if (_target := _target.strip()).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, + message=f"error: could not cast '{_target}' as int", + recoverable=True, + exit_code=2, + ) + continue + + async with TelegramClient(session, api_id, api_hash) as client: + for target in targets: + try: + if delete_last is False: + await client.send_message( + int(target), + message.read_text(encoding="utf-8"), + 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, + message=f"error: could not delete old message", + recoverable=True, + exit_code=3, + ) + continue + + # send new message + target_sent_message = await client.send_message( + target, + message.read_text(), + 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, + message=f"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: + validate_vars() + with TelegramClient(session, api_id, api_hash) as client: + client.start() + exit() + + +def list_chats() -> None: + validate_vars() + with TelegramClient(session, 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: + login() + + elif "list" in argv: + list_chats() + + else: + asyncio.run(run()) + + +if __name__ == "__main__": + entry() diff --git a/src/spow-telegram-bridge/poetry.lock b/src/spow-telegram-bridge/poetry.lock new file mode 100644 index 0000000..88e11cc --- /dev/null +++ b/src/spow-telegram-bridge/poetry.lock @@ -0,0 +1,244 @@ +# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. + +[[package]] +name = "black" +version = "23.10.1" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-23.10.1-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:ec3f8e6234c4e46ff9e16d9ae96f4ef69fa328bb4ad08198c8cee45bb1f08c69"}, + {file = "black-23.10.1-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:1b917a2aa020ca600483a7b340c165970b26e9029067f019e3755b56e8dd5916"}, + {file = "black-23.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c74de4c77b849e6359c6f01987e94873c707098322b91490d24296f66d067dc"}, + {file = "black-23.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b4d10b0f016616a0d93d24a448100adf1699712fb7a4efd0e2c32bbb219b173"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:b15b75fc53a2fbcac8a87d3e20f69874d161beef13954747e053bca7a1ce53a0"}, + {file = "black-23.10.1-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:e293e4c2f4a992b980032bbd62df07c1bcff82d6964d6c9496f2cd726e246ace"}, + {file = "black-23.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d56124b7a61d092cb52cce34182a5280e160e6aff3137172a68c2c2c4b76bcb"}, + {file = "black-23.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:3f157a8945a7b2d424da3335f7ace89c14a3b0625e6593d21139c2d8214d55ce"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:cfcce6f0a384d0da692119f2d72d79ed07c7159879d0bb1bb32d2e443382bf3a"}, + {file = "black-23.10.1-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:33d40f5b06be80c1bbce17b173cda17994fbad096ce60eb22054da021bf933d1"}, + {file = "black-23.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:840015166dbdfbc47992871325799fd2dc0dcf9395e401ada6d88fe11498abad"}, + {file = "black-23.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:037e9b4664cafda5f025a1728c50a9e9aedb99a759c89f760bd83730e76ba884"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:7cb5936e686e782fddb1c73f8aa6f459e1ad38a6a7b0e54b403f1f05a1507ee9"}, + {file = "black-23.10.1-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:7670242e90dc129c539e9ca17665e39a146a761e681805c54fbd86015c7c84f7"}, + {file = "black-23.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ed45ac9a613fb52dad3b61c8dea2ec9510bf3108d4db88422bacc7d1ba1243d"}, + {file = "black-23.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:6d23d7822140e3fef190734216cefb262521789367fbdc0b3f22af6744058982"}, + {file = "black-23.10.1-py3-none-any.whl", hash = "sha256:d431e6739f727bb2e0495df64a6c7a5310758e87505f5f8cde9ff6c0f2d7e4fe"}, + {file = "black-23.10.1.tar.gz", hash = "sha256:1f8ce316753428ff68749c65a5f7844631aa18c8679dfd3ca9dc1a289979c258"}, +] + +[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)"] +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 = "isort" +version = "5.12.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"}, + {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"}, +] + +[package.extras] +colors = ["colorama (>=0.4.3)"] +pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"] +plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] + +[[package]] +name = "mypy" +version = "1.6.1" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c"}, + {file = "mypy-1.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb"}, + {file = "mypy-1.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e"}, + {file = "mypy-1.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f"}, + {file = "mypy-1.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c"}, + {file = "mypy-1.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5"}, + {file = "mypy-1.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245"}, + {file = "mypy-1.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183"}, + {file = "mypy-1.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0"}, + {file = "mypy-1.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7"}, + {file = "mypy-1.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f"}, + {file = "mypy-1.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660"}, + {file = "mypy-1.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7"}, + {file = "mypy-1.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71"}, + {file = "mypy-1.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a"}, + {file = "mypy-1.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169"}, + {file = "mypy-1.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143"}, + {file = "mypy-1.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46"}, + {file = "mypy-1.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85"}, + {file = "mypy-1.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45"}, + {file = "mypy-1.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208"}, + {file = "mypy-1.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd"}, + {file = "mypy-1.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332"}, + {file = "mypy-1.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f"}, + {file = "mypy-1.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30"}, + {file = "mypy-1.6.1-py3-none-any.whl", hash = "sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1"}, + {file = "mypy-1.6.1.tar.gz", hash = "sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +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.11.2" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"}, + {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"}, +] + +[[package]] +name = "platformdirs" +version = "3.11.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"}, + {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"}, +] + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] + +[[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.5.0" +description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +files = [ + {file = "pyasn1-0.5.0-py2.py3-none-any.whl", hash = "sha256:87a2121042a1ac9358cabcaf1d07680ff97ee6404333bacca15f76aa8ad01a57"}, + {file = "pyasn1-0.5.0.tar.gz", hash = "sha256:97b7290ca68e62a832558ec3976f15cbf911bf5d7c7039d8b861c2a0ece69fde"}, +] + +[[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 = "telethon" +version = "1.32.0" +description = "Full-featured Telegram client library for Python 3" +optional = false +python-versions = ">=3.5" +files = [ + {file = "Telethon-1.32.0.tar.gz", hash = "sha256:a7efe0da9002e545236d636833e29e1be460e268d20ca49d03f8b9e8c9dab82a"}, +] + +[package.dependencies] +pyaes = "*" +rsa = "*" + +[package.extras] +cryptg = ["cryptg"] + +[[package]] +name = "typing-extensions" +version = "4.8.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, + {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11" +content-hash = "be959138054d5d36d8a880f957864c6968bee660b7e8d376f1694b96274295ec" diff --git a/src/spow-telegram-bridge/pyproject.toml b/src/spow-telegram-bridge/pyproject.toml new file mode 100644 index 0000000..66b0da0 --- /dev/null +++ b/src/spow-telegram-bridge/pyproject.toml @@ -0,0 +1,34 @@ +[tool.poetry] +name = "spow-telegram-bridge" +version = "0.1.0" +description = "add-on bridge for surplus on wheels (s+ow) to telegram" +authors = ["Mark Joshwel "] +license = "Unlicense" +readme = "README.md" +packages = [ + {include = "bridge.py"} +] + +[tool.poetry.dependencies] +python = "^3.11" +Telethon = "^1.32.0" + +[tool.poetry.group.dev.dependencies] +black = "^23.10.1" +mypy = "^1.6.1" +isort = "^5.12.0" + +[tool.poetry.scripts] +spow-telegram-bridge = 'bridge:entry' +"s+ow-telegram-bridge" = 'bridge:entry' + +[tool.black] +line-length = 90 + +[tool.isort] +line_length = 90 +profile = "black" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/src/spow-whatsapp-bridge/.gitignore b/src/spow-whatsapp-bridge/.gitignore new file mode 100644 index 0000000..b62e1bb --- /dev/null +++ b/src/spow-whatsapp-bridge/.gitignore @@ -0,0 +1,7 @@ +spow-whatsapp-bridge +mdtest.db +dist + +# nix +.devbox +result \ No newline at end of file diff --git a/src/spow-whatsapp-bridge/LICENCE b/src/spow-whatsapp-bridge/LICENCE new file mode 100644 index 0000000..52d1351 --- /dev/null +++ b/src/spow-whatsapp-bridge/LICENCE @@ -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. + diff --git a/src/spow-whatsapp-bridge/build-termux.sh b/src/spow-whatsapp-bridge/build-termux.sh new file mode 100644 index 0000000..51d1d09 --- /dev/null +++ b/src/spow-whatsapp-bridge/build-termux.sh @@ -0,0 +1,4 @@ +pkg install golang +go build +mkdir -p $HOME/.local/bin +mv spow-whatsapp-bridge $HOME/.local/bin/s+ow-whatsapp-bridge diff --git a/src/spow-whatsapp-bridge/go.mod b/src/spow-whatsapp-bridge/go.mod new file mode 100644 index 0000000..fd891ef --- /dev/null +++ b/src/spow-whatsapp-bridge/go.mod @@ -0,0 +1,22 @@ +module spow-whatsapp-bridge + +go 1.21.3 + +require ( + github.com/mattn/go-sqlite3 v1.14.18 + github.com/mdp/qrterminal/v3 v3.2.0 + go.mau.fi/whatsmeow v0.0.0-20231104103606-23bd57d939ca + google.golang.org/protobuf v1.31.0 +) + +require ( + filippo.io/edwards25519 v1.0.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + go.mau.fi/libsignal v0.1.0 // indirect + go.mau.fi/util v0.2.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect + golang.org/x/sys v0.14.0 // indirect + golang.org/x/term v0.13.0 // indirect + rsc.io/qr v0.2.0 // indirect +) diff --git a/src/spow-whatsapp-bridge/go.sum b/src/spow-whatsapp-bridge/go.sum new file mode 100644 index 0000000..40a9ab8 --- /dev/null +++ b/src/spow-whatsapp-bridge/go.sum @@ -0,0 +1,42 @@ +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= +github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mdp/qrterminal v1.0.1/go.mod h1:Z33WhxQe9B6CdW37HaVqcRKzP+kByF3q/qLxOGe12xQ= +github.com/mdp/qrterminal/v3 v3.0.0 h1:ywQqLRBXWTktytQNDKFjhAvoGkLVN3J2tAFZ0kMd9xQ= +github.com/mdp/qrterminal/v3 v3.0.0/go.mod h1:NJpfAs7OAm77Dy8EkWrtE4aq+cE6McoLXlBqXQEwvE0= +github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk= +github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk= +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/util v0.2.0 h1:AMGBEdg9Ya/smb/09dljo9wBwKr432EpfjDWF7aFQg0= +go.mau.fi/util v0.2.0/go.mod h1:AxuJUMCxpzgJ5eV9JbPWKRH8aAJJidxetNdUj7qcb84= +go.mau.fi/whatsmeow v0.0.0-20230805111647-405414b9b5c0 h1:6kAOyrp8E9p99X1I3uj7BtEFspdcVjnYzUZpqcHo/mE= +go.mau.fi/whatsmeow v0.0.0-20230805111647-405414b9b5c0/go.mod h1:+ObGpFE6cbbY4hKc1FmQH9MVfqaemmlXGXSnwDvCOyE= +go.mau.fi/whatsmeow v0.0.0-20231104103606-23bd57d939ca h1:r1/XGlSlYUFR4WTDKURPf8bTuPncrADZfOGPNrfr1oI= +go.mau.fi/whatsmeow v0.0.0-20231104103606-23bd57d939ca/go.mod h1:u557d2vph8xcLrk3CKTBknUHoB6icUpqazA4w+binRU= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY= +rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs= diff --git a/src/spow-whatsapp-bridge/main.go b/src/spow-whatsapp-bridge/main.go new file mode 100644 index 0000000..88df671 --- /dev/null +++ b/src/spow-whatsapp-bridge/main.go @@ -0,0 +1,385 @@ +// Copyright (c) 2021 Tulir Asokan +// Copyright (c) 2023 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/. + +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/libsignal/groups" + // "go.mau.fi/libsignal/keys/message" + "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) + +var data_dir = path.Join(os.Getenv("HOME"), ".local", "share", "s+ow-whatsapp-bridge") +var sharetext_path = path.Join(os.Getenv("HOME"), ".cache", "s+ow", "message") + +func main() { + if *debugLogs { + logLevel = "DEBUG" + } + log = waLog.Stdout("Main", logLevel, true) + + // make and change dir + err := os.MkdirAll(data_dir, os.ModePerm) + if err != nil { + log.Errorf("s+ow-whatsapp-bridge: Failed to create directory: %v", err) + return + } + err = os.Chdir(data_dir) + if err != nil { + log.Errorf("s+ow-whatsapp-bridge: Failed to change directory: %v", err) + return + } + + // mdtest code + waBinary.IndentXML = true + flag.Parse() + + if *requestFullSync { + store.DeviceProps.RequireFullSync = proto.Bool(true) + } + + 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) + input := make(chan string) + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, 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 + } + } + }() + + // 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 + } + + // read file ~/.cache/s+ow/message + sharetext, err := os.ReadFile(sharetext_path) + if err != nil { + log.Errorf("s+ow-whatsapp-bridge: Failed to open file: %v", err) + return + } + + // "normal" operation; 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 + } + 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 + } else { + 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) { + switch cmd { + 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) + return + } + 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 ") + 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("s+ow-whatsapp-bridge: 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("s+ow-whatsapp-bridge: 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()) + path := fmt.Sprintf("%s%s", evt.Info.ID, exts[0]) + err = os.WriteFile(path, 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", path) + } + 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("App state event: %+v / %+v", evt.Index, evt.SyncActionValue) + case *events.KeepAliveTimeout: + log.Debugf("Keepalive timeout event: %+v", evt) + case *events.KeepAliveRestored: + log.Debugf("Keepalive restored") + } +} diff --git a/src/surplus-on-wheels/.s+ow-bridges b/src/surplus-on-wheels/.s+ow-bridges new file mode 100644 index 0000000..5b9b6ed --- /dev/null +++ b/src/surplus-on-wheels/.s+ow-bridges @@ -0,0 +1,2 @@ +SPOW_TELEGRAM_API_HASH="" SPOW_TELEGRAM_API_ID="" s+ow-telegram-bridge +s+ow-whatsapp-bridge diff --git a/src/surplus-on-wheels/README.md b/src/surplus-on-wheels/README.md new file mode 100644 index 0000000..f71c11b --- /dev/null +++ b/src/surplus-on-wheels/README.md @@ -0,0 +1,284 @@ +# surplus on wheels + +surplus on wheels (s+ow) is a pure shell script to get your location using +`termux-location`, process it through [surplus](https://github.com/markjoshwel/surplus), +and send it to messaging service or wherever using [bridges](#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](#installing) + - [as a standalone script](#as-a-standalone-script) + - [as a cron job](#as-a-cron-job) + - [using installation scripts](#using-installation-scripts) +- [usage](#usage) + - [environment variables](#environment-variables) + - [faking locations](#faking-locations) +- [bridges](#bridges) + - [bring your own bridge](#bring-your-own-bridge) +- [licence](#licence) + +## 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 the Termux:API app from [F-Froid](https://f-droid.org/en/packages/com.termux.api/). + +2. install pipx + + ```text + pip install pipx + ``` + +3. install surplus: + + ```text + pip install https://github.com/markjoshwel/surplus/releases/latest/download/surplus-latest-py3-none-any.whl + ``` + +4. install surplus on wheels: + + ```text + mkdir -p ~/.local/bin/ + curl https://raw.githubusercontent.com/markjoshwel/surplus-on-wheels/main/s+ow > ~/.local/bin/s+ow + chmod +x ~/.local/bin/s+ow + ``` + +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). + +### 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: + + > [!IMPORTANT] + > minimally fill in the `SPOW_TARGETS` variable before running s+ow. + > [(see usage for more info)](#usage) + + run the following command: + + ```text + crontab -e + ``` + + and add the following text: + + ```text + 59 * * * * bash -l -c "SPOW_TARGETS="" SPOW_CRON=y s+ow" + ``` + + 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 you’re new to cron jobs. + +if you haven’t already, [install an appropriate bridge](#bridges) 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 instructiions in +> [as a cron job](#as-a-cron-job). + +> [!IMPORTANT] +> if not installed already, install [Termux:API](https://f-droid.org/en/packages/com.termux.api/) +> from F-Droid. + +1. setup s+ow: + + ```text + curl https://raw.githubusercontent.com/markjoshwel/surplus-on-wheels/main/termux-s+ow-setup | sh + ``` + +2. restart termux! + +3. setup cron job: + + ```text + curl https://raw.githubusercontent.com/markjoshwel/surplus-on-wheels/main/termux-s+ow-setup-cron | sh + ``` + + the script will run `crontab -e`, and you can then edit the variables as per your + needs. minimally fill in the `SPOW_TARGETS` variable before running s+ow. + see [usage](#usage) for more information. + +et voilà! s+ow is now setup. to actually send the message to a messaging platform, +[install an appropriate bridge](#bridges). + +## usage + +### environment variables + +s+ow uses three environment variables, two of which are optional: + +1. `SPOW_TARGETS` + a single line of comma-deliminated 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](https://github.com/markjoshwel/spow-whatsapp-bridge), and the + Telegram chat ID is `tg:`-prefixed as recognised by the + [spow-telegram-bridge](https://github.com/markjoshwel/spow-telegram-bridge). + +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 will delay itself appropriately. + + setting it to `n` will also be treated as empty. + +3. `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 you’re in a + country with cell towers close by or everywhere, like Singapore. + + setting it to `n` will also be treated as empty. + +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. + +4. `LOCATION_TIMEOUT` (optional) + set as a number to override the default first location timeout of `50`. + +### 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. + +## bridges + +there are two “official” bridges for s+ow: + +- [spow-whatsapp-bridge](https://github.com/markjoshwel/spow-whatsapp-bridge) +- [spow-telegram-bridge](https://github.com/markjoshwel/spow-telegram-bridge) + +bridges can be located anywhere, as long as they are reachable by the shell s+ow is +running in. + +s+ow will run the bridges through definitions in in `$HOME/.s+ow-bridges`. +each line of `$HOME/.s+ow-bridges` is evaluated as a shell command, and is piped +`SPOW_TARGETS`. + +> [!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. + +### bring your own bridge + +custom bridges are relatively easy as they are: + +1. an executable or script + +2. that reads `SPOW_TARGETS` (see [usage](#usage)) from stdin + + - bridges should account for the possibility of comma and space (`, ` instead of just + `,`) delimited targets, and strip each target of preceding and trailing whitespace. + + - bridges should recognise a platform based on a prefix + (e.g. `wa:` for WhatsApp, `tg:` for Telegram, etc.) + + - bridges do not need to account for the possibility of multiple lines sent to stdin. + +3. reads `SPOW_MESSAGE` (`~/.cache/spow/message`) for the message content + +notes: + +1. stderr and stdout are redirected to s+ow’s error and output logs respectively. +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//`, or similar. + +## licence + +surplus on wheels is free and unencumbered software released into the public domain. +for more information, please refer to [UNLICENCE](/UNLICENCE) or . diff --git a/src/surplus-on-wheels/UNLICENCE b/src/surplus-on-wheels/UNLICENCE new file mode 100644 index 0000000..68a49da --- /dev/null +++ b/src/surplus-on-wheels/UNLICENCE @@ -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 diff --git a/src/surplus-on-wheels/s+ow b/src/surplus-on-wheels/s+ow new file mode 100644 index 0000000..9e3ef5b --- /dev/null +++ b/src/surplus-on-wheels/s+ow @@ -0,0 +1,483 @@ +#!/bin/sh + +# surplus on wheels (s+ow) - a pure shell script to run surplus with mdtest using the termux-api +# ------------------------ +# public domain, unlicence + +# shellcheck disable=SC2059 +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} + +# per-tool session logs +SPOW_NETLC_OUT="$SPOW_CACHE_DIR/location.net.json" +SPOW_GPSLC_OUT="$SPOW_CACHE_DIR/location.gps.json" +SPOW_LOCTN_OUT="$SPOW_CACHE_DIR/location.json" +SPOW_SPLUS_OUT="$SPOW_CACHE_DIR/surplus.out.log" +SPOW_SPLUS_ERR="$SPOW_CACHE_DIR/surplus.err.log" + +# per-session collated logs +SPOW_SESH_OUT="$SPOW_CACHE_DIR/out.log" +SPOW_SESH_ERR="$SPOW_CACHE_DIR/err.log" + +# 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" + +# 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 + +# ensure commands exist +if ! command -v termux-location >/dev/null 2>&1; 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" + exit 1 +fi + +if ! command -v surplus >/dev/null 2>&1; then + printf "s+ow: error: surplus is not installed.\ninstall it with 'pip install https://github.com/markjoshwel/surplus/releases/latest/download/surplus-latest-py3-none-any.whl'\n" + 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" + +# 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 + ( + termux-location -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 + ( + termux-location -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 termux-location" | 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 proritised: 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 -td "$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 "$fake_rest\n" >"$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 '$SPOW_BRIDGES' file; message is not sent.\n" + 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 " $(date)\n" + 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 termux-location" "$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 + + time_locate_end="$(date +%s)" + time_locate=$((time_locate_end - time_locate_start)) + + time_surplus_start="$(date +%s)" + + # surplus + printf "running surplus... " + notify "Running surplus -td $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 " $(date)\n" + sleep 1 + done + printf "proceeding\n" + fi + + time_sendmsg_start="$(date +%s)" + + # mdtest/send message + printf "sending message(s)... " + notify "Sending message(s)" + sent_type=0 # 0 for freshly made sharetext + # 1 for recycling a last location + # 2 for using fallback template + 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 + # shellcheck disable=SC2059 + sharetext="$(printf "$LOCATION_FALLBACK" "$status" "$locate_run" "$sent_type")" + sent_type=2 + 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 + + 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 1 +fi +run + +printf "%s\n\n" "$(cat "$SPOW_SESH_OUT")" >>"$SPOW_WEEK_OUT" +printf "%s\n\n" "$(cat "$SPOW_SESH_ERR")" >>"$SPOW_WEEK_ERR" diff --git a/src/surplus-on-wheels/termux-s+ow-setup b/src/surplus-on-wheels/termux-s+ow-setup new file mode 100644 index 0000000..b43924a --- /dev/null +++ b/src/surplus-on-wheels/termux-s+ow-setup @@ -0,0 +1,22 @@ +#!/bin/sh +set -e + +# get packages +yes | pkg upgrade +yes | pkg install python cronie termux-api termux-services wget + +# install pipx +pip install pipx + +# install surplus +pipx install https://github.com/markjoshwel/surplus/releases/latest/download/surplus-latest-py3-none-any.whl + +# install s+ow +mkdir -p ~/.local/bin/ +curl https://raw.githubusercontent.com/markjoshwel/surplus-on-wheels/main/s+ow > ~/.local/bin/s+ow +chmod +x ~/.local/bin/s+ow + +# setup path +echo "export PATH=\$PATH:\$HOME/.local/bin/" >> ~/.profile + +printf "\ndone\n" diff --git a/src/surplus-on-wheels/termux-s+ow-setup-cron b/src/surplus-on-wheels/termux-s+ow-setup-cron new file mode 100644 index 0000000..2c2adef --- /dev/null +++ b/src/surplus-on-wheels/termux-s+ow-setup-cron @@ -0,0 +1,12 @@ +#!/bin/sh + +# enable cron service and add to crontab +sv-enable crond +printf "59 * * * *\tSPOW_TARGETS=\"\" SPOW_CRON=y ~/.local/bin/s+ow\n" > s+ow.cron +crontab s+ow.cron +rm s+ow.cron + +# open editor +crontab -e + +printf "\ndone\n" diff --git a/src/surplus/UNLICENCE b/src/surplus/UNLICENCE new file mode 100644 index 0000000..00d2e13 --- /dev/null +++ b/src/surplus/UNLICENCE @@ -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 \ No newline at end of file diff --git a/surplus/__init__.py b/src/surplus/__init__.py similarity index 100% rename from surplus/__init__.py rename to src/surplus/__init__.py diff --git a/surplus/py.typed b/src/surplus/py.typed similarity index 100% rename from surplus/py.typed rename to src/surplus/py.typed diff --git a/surplus/surplus.py b/src/surplus/surplus.py similarity index 99% rename from surplus/surplus.py rename to src/surplus/surplus.py index d1639cf..4b9c61e 100644 --- a/surplus/surplus.py +++ b/src/surplus/surplus.py @@ -69,16 +69,16 @@ if TYPE_CHECKING: # constants -__version__ = "2024.0.0-alpha" +__version__ = "2024.0.0-beta" VERSION: Final[tuple[int, int, int]] = (2024, 0, 0) -VERSION_SUFFIX: Final[str] = "-local" +VERSION_SUFFIX: Final[str] = "-beta-local" BUILD_BRANCH: Final[str] = "future" BUILD_COMMIT: Final[str] = "latest" BUILD_DATETIME: Final[datetime] = datetime.now(timezone(timedelta(hours=8))) # using SGT CONNECTION_MAX_RETRIES: int = 9 CONNECTION_WAIT_SECONDS: int = 10 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 SHAREABLE_TEXT_LINE_0_KEYS: dict[str, tuple[str, ...]] = {