Skip to content

the developer's handbook

Abstract

i (mark), heavily use nix to manage my projects, either with devbox or flakes

if you are going to develop for surplus or its sibling projects (except surplus on wheels, which only needs shfmt and shellcheck), i would recommend you install Nix using Determinate Systems' Nix Installer:

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install

if i, a very very inexperienced Nix and NixOS user to give a rundown on why to use it:

  1. environments and builds are reproducible

    nix is part package manager, operating system (as NixOS), and functional programming language. because it's functional, having X as an input will always produce Y as an output, no matter what. even in the event of environmental, social, economic, or structural collapse

    what does this mean for you?
    when i use nix develop and you use nix develop to start a development environment, your version of python will be the same as my version of python. your version of go will be same as my version of go, etc

    if i can build it, and the locked inputs of the nix flake are still available on the internet, you can build it too

  2. the nix store, located literally at /nix/store
    it's where it stores every package you need, separate and isolated from other packages. lets say you have a tool that needs python (3.8), and another tool that needs python (3.11). nix store will download and store the binaries for both python installations, instead of sharing the earliest downloaded python version for both tools

    what does this mean for you?
    whatever project you're working on that can use nix for development environments and builds will not dirty anything else on your system. any build dependencies of surplus provided with nix develop will not mess up software installed for other projects or even the system. neat, if you ask me tbh

tl;dr: things will just werk with nix. but if you see all of this and go, "eh. i can manage what i install.", then power to you! i list down exactly what prerequisite software needs to be installed for each project anyways, so have fun! :)

surplus (and Documentation)

environment setup

Note

all prerequisite software are available in a nix flake. if you want a reproducible environment, run nix develop --impure in the repository root

  • for NixOS users, nix-ld is needed. if you use flakes to declare your system, follow accordingly. else, use /etc/nixos/configuration.nix

    for NixOS-on-WSL users, use nix-ld-rs

    once you're done installing nix-ld or nix-ld-rs, don't forget to run sudo nixos-rebuild switch

prerequisite software:

  • Python, 3.11 or newer
  • Hatch: dependency management and build tool

to start a development environment:

hatch shell

for docs:

hatch -e docs shell

workflow for python code

after modifying Python code:

  1. format the source:

    hatch fmt -f
    
  2. run static checks:

    hatch run check
    
  3. manually exercise the CLI against at least one query form touched by your change:

    hatch run surplus -d "Wisma Atria"
    hatch run surplus -c latlong "8QMF+FX Singapore"
    
  4. if the change affects shareable-text generation, compare the output with a few known addresses and include debug output when filing or reviewing issues

the 2024.0.0-beta refactor did not add a dedicated Python test suite before the project was sunset, so the best available workflow is formatter, static checks, and targeted manual CLI runs.

workflow for markdown documentation

run the documentation server with:

hatch run docs:serve

i personally don't use a linter for markdown files, if it looks good on my code editor, then whatever. if you're going to contribute back, i ask for three things:

  • run it through a spell checker or something similar

  • line limit of 100

  • should be readable as-is on a code editor, not the markdown preview pane.

    my stance is, if you can afford a fancy preview of the markdown file, use the nice-ned documentation website. else, read it as a plaintext file

    (make it look pretty on the doc site and in plaintext)

sharetext technical details

Note

this is a breakdown of surplus's output when converting to shareable text. when converting to other output types, output may be different.

$ 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

debug output

  • behaviour.query, split_query, and original_query
    behaviour.query is the original query string or a list of strings from space-splitting the original query string passed to parse_query() for parsing.

split_query is the original query string split by spaces.

original_query is a single non-split string.

$ s+ Temasek Polytechnic
     -------------------
     query

behaviour.query -> ['Temasek', 'Polytechnic']
split_query     -> ['Temasek', 'Polytechnic']
original_query  -> 'Temasek Polytechnic'
  • 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 respectively.

  • query
    query is a variable of type Result[Query]. this variable is displayed to show what query type parse_query() has recognised, and if there were any errors during query parsing.

  • latlong_result.get()
    only shown if the query is a Plus Code. the latitude longitude coordinates derived from the Plus Code.

  • location
    the response dictionary from the reverser function passed to surplus().

  • split_iso3166_2 and special key arrangements
    a list of strings containing the split iso3166-2 code, meaning the country/subdivision identifier.

if special key arrangements are available for the code, a line similar to the following will be shown:

debug: _generate_text: using special key arrangements for 'SG-03' (Singapore)
  • 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

#                           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, general regional location, to reduce repeated elements found in seen_names.

reasoning is, if an element on line 4 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.
filter=lambda ak: [
    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.

  • 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.

line breakdown

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:

The University of Queensland
Ngee Ann Polytechnic
Botanic Gardens

Nominatim keys:

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:

Novena Square Office Tower A
Visitor Centre

Nominatim key:

building

2. highway name

examples:

Marina Coastal Expressway
Lornie Highway

Nominatim key:

highway

3. block/house/building number, house name, road

examples:

535 Clementi Road
Macquarie Street
Braddell Road

Nominatim keys:

house_number, house_name, road

4. general regional location

examples:

St Lucia, Greater Brisbane
The Drag, Austin
Toa Payoh Crest

Nominatim keys:

residential, neighbourhood, allotments, quarter, city_district, district, borough,
suburb, subdivision, municipality, city, town, village

5. postal code

examples:

310131
78705
4066

Nominatim key:

postcode

6. general global information

examples:

Travis County, Texas, United States
Southeast, Singapore
Queensland, Australia

Nominatim keys:

region, county, state, state_district, country, continent

api reference

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 object containing version and build information, set by src/tools/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, ...]]
    SHAREABLE_TEXT_LINE_SETTINGS: dict[str, dict[int, tuple[str, bool]]]
    SHAREABLE_TEXT_NAMES: dict[str, tuple[str, ...]]
    SHAREABLE_TEXT_LOCALITY: dict[str, tuple[str, ...]]
  • 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)

types

Query
Query: typing.TypeAlias = PlusCodeQuery | LocalCodeQuery | LatlongQuery | StringQuery

type alias representing either a PlusCodeQuery, LocalCodeQuery, LatlongQuery or StringQuery

ResultType
ResultType = TypeVar("ResultType")

generic type used by Result

SurplusGeocoderProtocol

typing_extensions.Protocol class for documentation and static type checking of surplus geocoder functions

  • signature and conforming function signature
class SurplusGeocoderProtocol(Protocol):
    def __call__(self, place: str) -> Latlong:
        ...

functions that conform to this protocol should have the following signature:

def example(place: str) -> Latlong: ...
  • information on conforming functions

function takes in a location name as a string, and returns a Latlong.

function MUST supply a bounding_box attribute to the to-be-returned Latlong. the bounding box is used when surplus shortens Plus Codes.

function can and should be at minimum functools.lru_cache()-wrapped if the geocoding service asks for caching

exceptions are handled by the caller

SurplusReverserProtocol

typing_extensions.Protocol class for documentation and static type checking of surplus reverser functions

  • signature and conforming function signature
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:

def example(latlong: Latlong, level: int = 18) -> dict[str, Any]: ...
  • information on conforming functions

function takes in a Latlong object and return a dictionary with SHAREABLE_TEXT_LINE_*_KEYS 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.

{
    '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 if the geocoding service asks for caching

exceptions are handled by the caller

class Behaviour

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 for more information

  • reverser: SurplusReverserProtocol = default_geocoding.reverser
    Latlong object to address information dictionary function, see SurplusReverserProtocol for more information

  • stderr: typing.TextIO = sys.stderr
    TextIO-like object representing a writeable file. defaults to sys.stderr.

  • stdout: typing.TextIO = sys.stdout
    TextIO-like object representing a writeable file. defaults to 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 fingerprinted user agent and exit

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 and SurplusReverserProtocol for more information how to implement a compliant custom geocoder functions.

dataclasses.dataclass providing the default geocoding functionality for surplus, via OpenStreetMap Nominatim

attributes

  • user_agent: str = default_fingerprint
    pass in a custom user agent here, else it will be the default fingerprinted user agent

example usage

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

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
def update_geocoding_functions(self) -> None: ...
SurplusDefaultGeocoding.geocoder()

[!WARNING]
this function is primarily given to be passed into a Behaviour object, and is not meant to be called directly.

default geocoder for surplus

see SurplusGeocoderProtocol for more information on surplus geocoder functions

SurplusDefaultGeocoding.reverser()

[!WARNING]
this function is primarily given to be passed into a Behaviour object, and is not meant to be called directly.

default reverser for surplus

see SurplusReverserProtocol for more information on surplus reverser functions

class ConversionResultTypeEnum

enum.Enum 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 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

# 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

Result.__bool__()

method that returns True if self.error is None

  • signature
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
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
def get(self) -> ResultType: ...
  • returns self.value

class Latlong

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

Latlong.__str__()

method that returns a comma-and-space-separated string of self.latitude and self.longitude

  • signature
def __str__(self) -> str: ...
  • returns str

class PlusCodeQuery

typing.NamedTuple representing a full-length Plus Code (e.g., 6PH58QMF+FX)

attributes

  • code: str

methods

PlusCodeQuery.to_lat_long_coord()
  • signature
def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]:
    ...
PlusCodeQuery.__str__()

method that returns string representation of query

  • signature
def __str__(self) -> str: ...
  • returns str

class LocalCodeQuery

typing.NamedTuple representing a shortened Plus Code 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

LocalCodeQuery.to_full_plus_code()

exclusive method that returns a full-length Plus Code as a string

  • signature
def to_full_plus_code(self, geocoder: SurplusGeocoderProtocol) -> Result[str]:
    ...
  • arguments

  • geocoder: SurplusGeocoderProtocol
    name string to location function, see SurplusGeocoderProtocol for more information

  • returns Result[str]

LocalCodeQuery.to_lat_long_coord()

method that returns a latitude-longitude coordinate pair

  • signature
def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]:
    ...
LocalCodeQuery.__str__()

method that returns string representation of query

  • signature
def __str__(self) -> str: ...
  • returns str

class LatlongQuery

typing.NamedTuple representing a latitude-longitude coordinate pair

attributes

  • latlong: Latlong

methods

LatlongQuery.to_lat_long_coord()

method that returns a latitude-longitude coordinate pair

  • signature
def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]:
    ...
LatlongQuery.__str__()

method that returns string representation of query

  • signature
def __str__(self) -> str: ...
  • returns str

class StringQuery

typing.NamedTuple representing a pure string query

attributes

  • query: str

methods

StringQuery.to_lat_long_coord()

method that returns a latitude-longitude coordinate pair

  • signature
def to_lat_long_coord(self, geocoder: SurplusGeocoderProtocol) -> Result[Latlong]:
    ...
StringQuery.__str__()

method that returns string representation of query

  • signature
def __str__(self) -> str: ...
  • returns str

def surplus()

query to shareable text conversion function

  • signature
def surplus(query: Query | str, behaviour: Behaviour) -> Result[str]: ..

def parse_query()

function that parses a query string into a query object

  • signature
def parse_query(behaviour: Behaviour) -> Result[Query]: ...

def generate_fingerprinted_user_agent()

function that attempts to return a unique user agent string.

  • signature
def generate_fingerprinted_user_agent() -> Result[str]:

this result will always have a valid value as erroneous results will have a resulting value of 'surplus/<version> (generic-user)'

valid results will have a value of 'surplus/<version> (<fingerprint hash>)', 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:

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
2.2.0-local
  1. system_info - generic machine and operating system information
Linux-6.5.0-locietta-WSL2-xanmod1-x86_64-with-glibc2.35
  1. hostname - your computer's hostname
mark
  1. mac_address - your computer's mac address
A9:36:3C:98:79:33

after hashing, this string becomes a 12 character hexadecimal string, as shown below:

surplus/2.2.0-local (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/<version> (generic-user)'.

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.

$ surplus --user_agent "a-shiny-custom-and-unique-user-agent" 77Q4+7X Austin, Texas, USA
...
>>> from surplus import surplus, Behaviour
>>> surplus(..., Behaviour(user_agent="a-shiny-custom-and-unique-user-agent"))
...

surplus on wheels

environment setup

Note

all prerequisite software are available in a nix flake. if you want a reproducible environment, run nix develop in src/surplus-on-wheels

prerequisite software:

workflow

Note

alternatively, run check.sh inside src/surplus-on-wheels

  • formatting s+ow:

    • run shfmt s+ow > s+ow.new
    • mv s+ow.new into s+ow
      sometimes when piping shfmt's output immediately into the same file results in the file being empty :(
  • checking s+ow:

    • run shellcheck s+ow
      if there's no output, that means it passed :)
  • if commiting back into the repository, try it out on your Termux system for a day or two, just to make sure it runs correctly


surplus on wheels: Telegram Bridge

environment setup

Note

all prerequisite software are available in a nix flake. if you want a reproducible environment, run nix develop in src/spow-telegram-bridge. it uses poetry2nix, so you won't need to run poetry shell afterwards. if you've changed the pyproject.toml file, just exit and re-run nix develop

prerequisite software:

  • Python, 3.11 or newer
  • Poetry: dependency management and build tool

to start a development environment:

poetry shell

workflow

after modifying,

  1. check the source code:

    1. mypy bridge.py
    2. ruff format bridge.py
    3. ruff check bridge.py

    Note

    alternatively, run check.sh inside src/spow-telegram-bridge

  2. and then test the binary

if the bridge behaves nominally, bump the version and commit!


surplus on wheels: WhatsApp Bridge

environment setup

Note

all prerequisite software are available in a nix flake. if you want a reproducible environment, run nix develop in src/spow-whatsapp-bridge

the flake will pull in the Android SDK and NDK for building on Termux, and as such can only be ran on x86_64-linux and x86_64-darwin

prerequisite software:

workflow for modifying bridge code

the bridge's code is just modified mdtest code, and as such, whenever in doubt, do a diff between mdtest and the bridge code

after modifying,

  1. check the source code:

    1. go fmt bridge.go
    2. go vet bridge.go
    3. golint bridge.go

    Note

    alternatively, run check.sh inside src/spow-whatsapp-bridge

  2. build a binary

  3. test the binary

  4. and if all goes well, bump the version and commit!

workflow for bumping dependencies

  • check with your editor, plugin, or online if there's newer patch/minor (see semantic versioning) versions to update to

  • change the go.mod accordingly

after bumping,

  1. build a binary
  2. test the binary
  3. and if all goes well, bump the version and commit!

workflow for building a binary

ensure you already have c compiler on the system (if you're using nix develop then yes you do), then run:

CGO_ENABLED=1 go build

nix users can alternatively run:

nix build

instructions to build a Termux build are located at the bridges' documentation page, however nix users can run the following instead for a reproducible, deterministic and hermetic build command:

nix build .#termux
the resulting build will be in result/spow-whatsapp-bridge

workflow for testing the binary

  • test it out, making sure that you write dummy test text to ~/.cache/s+ow/message before running the binary

    1. run s+ow-whatsapp-bridge login first

    2. run s+ow-whatsapp-bridge list if you don't already have a chat ID to send the test message to

    3. run s+ow-whatsapp-bridge type or copy and paste in a wa:-prefixed chat ID after it logs in, and verify it sends

if the bridge behaves nominally, bump the version and commit!

workflow for versioning and tagging releases

versioning surplus

format: YEAR.MAJOR.MINOR[-PRERELEASE] (semantic versioning)
example: 2024.0.0, 2024.0.0-beta
change: update the __version__ variable in src/surplus/surplus.py

versioning surplus on wheels

i've tried to make surplus on wheels as reliable as it could be given a POSIX compliant shell and commands you'd find available on virtually every linux system, Termux included

as such, it doesn't really follow a versioning scheme as it doesn't need to. also there's no automatic updater for it, which would be overkill anyway

versioning surplus on wheels: Telegram Bridge

format: REVISION.YYYY.WW[+BUILD] (calendar versioning)
example: 2.2024.24, 2.2024.24+1
change: version key in src/spow-telegram-bridge/pyproject.toml

REVISION here meaning any general revision/change

the Telegram Bridge relies on Telethon, which also follows semantic versioning. so, as long as major isn't bumped, or as long as Telegram doesn't become Discord, the MTProto APIs to talk to Telegram should be stable.

however because Telethon also relies on a bunch of networking libraries, it made some sense to still do weekly builds to bump dependencies, getting pipx to download the newest compatible dependencies as compared to dubiously running some sort of script to pipx inject dependencies

under normal circumstances, a non-working version of the bridge would and should not have a version bump. but for any reason if an already tagged bridge is faulty and/or erroneous in normal/expected usage, add a revision number to the end after a period (see example above)

versioning surplus on wheels: WhatsApp Bridge

format: REVISION.YYYY.WW[+BUILD] (calendar versioning)
example: 2.2024.25, 2.2024.25+1
change: version attribute of bridge attribute set in src/spow-whatsapp-bridge/flake.nix

REVISION here meaning any general revision/change

the WhatsApp Bridge relies on whatsmeow, a rolling release library due to the volatile, undocumented nature of WhatsApp's multidevice API and also directly and indirectly relies on a bunch of networking libraries:

src/spow-whatsapp-bridge/go.mod
module forge.joshwel.co/mark/surplus/src/spow-whatsapp-bridge

go 1.22.3

require (
    github.com/mattn/go-sqlite3 v1.14.24
    github.com/mdp/qrterminal/v3 v3.2.0
    go.mau.fi/whatsmeow v0.0.0-20240716084021-eb41d1f09552
    google.golang.org/protobuf v1.35.2
)

require (
    filippo.io/edwards25519 v1.1.0 // indirect
    github.com/google/uuid v1.6.0 // indirect
    github.com/gorilla/websocket v1.5.3 // indirect
    github.com/mattn/go-colorable v0.1.13 // indirect
    github.com/mattn/go-isatty v0.0.20 // indirect
    github.com/rs/zerolog v1.33.0 // indirect
    go.mau.fi/libsignal v0.1.1 // indirect
    go.mau.fi/util v0.6.0 // indirect
    golang.org/x/crypto v0.25.0 // indirect
    golang.org/x/net v0.27.0 // indirect
    golang.org/x/sys v0.22.0 // indirect
    golang.org/x/term v0.22.0 // indirect
    rsc.io/qr v0.2.0 // indirect
)

as such, it uses a calendar versioning scheme and is built weekly

under normal circumstances, a non-working version of the bridge would and should not have a version bump. but for any reason if an already tagged bridge is faulty and/or erroneous in normal/expected usage, add a revision number to the end after a period (see example above)


i've made my changes. what now?

if you're contributing back to surplus and/or the sibling projects, firstly, thanks! see the contributor's handbook for what's next