Gemini Protocol

An asset by mocha
The page banner background of a mountain and forest
Gemini Protocol hero image

Quick Information

0 ratings
Gemini Protocol icon image
mocha
Gemini Protocol

Make requests with the Gemini protocol (https://geminiprotocol.net/) & easily parse gemtext

Supported Engine Version
4.0
Version String
1.0.0
License Version
MIT
Support Level
community
Modified Date
2 months ago
Git URL
Issue URL

Godot 4 Gemini Protocl

Godot 4 addon for making requests over Gemini Protocol & parsing Gemtext documents

(not yet) Available on the asset library

Note that for godot reasons, client certificates cannot be used currently. This means that any page that has an AUTH wall cannot be accessed even if you have the correct certificate on hand.

Requesting Gemini documents

GeminiRequest node

Used to fetch documents from gemini servers

Properties:

  • port:int: Port to connect to the serverwith (defaults to 1965)
  • follow_redirects:bool: Automatically follow redirects without exiting (defaults to true)
  • max_redirects:int: Maximum amount of redirects to follow before aborting (defaults to 8)
  • timeout:float: Maximum amount of time a request may take. Set to 0 for infinite time (defaults to 0)

Functions:

  • request(url:String, server_certificate:X509Certificate=null) Request a page at the URL URL should be a url prefixed with gemini:// server_cetificate is used to verify self-signed certificates. Gemini protocl recommends using certificate pinning. Set to null to disable certificate verification (!!!) Awaiting this is the same as awaiting the request_completed signal
  • cancel_request(): Cancel current ongoing requests
  • static make_input_url(base_url:String, input:String)->String: Add an input query to a normal URL for when the server returns a INPUT response

Signals:

  • request_completed(result:GeminiRequestResult): Main signal to retreive the request result

Enums:
RESPONSECODES

  • 10: INPUT:
    • The basic input status code. A client MUST prompt a user for input, it should be URI-encoded per [STD66] and sent as a query to the same URI that generated this response.
  • 11: INPUT_SENSITIVE:
    • As per status code 10, but for use with sensitive input such as passwords. Clients should present the prompt as per status code 10, but the user's input should not be echoed to the screen to prevent it being read by "shoulder surfers".
  • 20: SUCCESS:
    • The server has successfully parsed and understood the request, and will serve up content of the given MIME type.
  • 30: REDIRECT_TEMPORARY:
    • The basic redirection code. The redirection is temporary and the client should continue to request the content with the original URI.
  • 31: REDIRECT_PERMANENT:
    • The location of the content has moved permanently to a new location, and clients SHOULD use the new location to retrieve the given content from then on.
  • 40: TEMPFAIL:
    • An unspecified condition exists on the server that is preventing the content from being served, but a client can try again to obtain the content.
  • 41: TEMPFAIL_SERVER_UNAVAILABLE:
    • The server is unavailable due to overload or maintenance. (cf HTTP 503)
  • 42: TEMPFAIL_CGI_ERROR:
    • A CGI process, or similar system for generating dynamic content, died unexpectedly or timed out.
  • 43: TEMPFAIL_PROXY_ERROR:
    • A proxy request failed because the server was unable to successfully complete a transaction with the remote host. (cf HTTP 502, 504)
  • 44: TEMPFAIL_SLOW_DOWN:
    • The server is requesting the client to slow down requests, and SHOULD use an exponential back off, where subsequent delays between requests are doubled until this status no longer returned.
  • 50: PERMFAIL:
    • This is the general permanent failure code.
  • 51: PERMFAIL_NOT_FOUND:
    • The requested resource could not be found (you can't find things at Area 51) and no further information is available. It may exist in the future, it may not. Who knows?
  • 52: PERMFAIL_GONE:
    • The resource requested is no longer available and will not be available again. Search engines and similar tools should remove this resource from their indices. Content aggregators should stop requesting the resource and convey to their human users that the subscribed resource is gone. (cf HTTP 410)
  • 53: PERMFAIL_PROXY_REQUEST_REFUSED:
    • The request was for a resource at a domain not served by the server and the server does not accept proxy requests.
  • 59: PERMFAIL_BAD_REQUEST:
    • The server was unable to parse the client's request, presumably due to a malformed request, or the request violated a contraint.
  • 60: AUTH:
    • The content requires a client certificate. The client MUST provide a certificate in order to access the content and SHOULD NOT repeat the request without one. The scope of a certificate generated in response to this status code should is limited to the host and port from which the status code was received and the path of the URL in the original request plus all paths below it. A server MAY require a different certificate for a different path on the same host and port. A server SHOULD allow the same certificate to be used for any content along the given path.
  • 61: AUTH_CERTIFICATE_NOT_AUTHORIZED:
    • The supplied client certificate is not authorised for accessing the particular requested resource. The problem is not with the certificate itself, which may be authorised for other resources.
  • 62: AUTH_CERTIFICATE_NOT_VALID:
    • The supplied client certificate was not accepted because it is not valid. This indicates a problem with the certificate in and of itself, with no consideration of the particular requested resource. The most likely cause is that the certificate's validity start date is in the future or its expiry date has passed, but this code may also indicate an invalid signature, or a violation of a X509 standard requirements.
  • 99: NONE:
    • This is not a gemini response code, but is used when the request fails before it is properly completed.

GeminiRequestResult resources

This resource is returned by GeminiRequest node's request_completed signal.
It has all the returned data by the server, if an error occured, etc etc.

Properties:

  • url:String: The URL requested
  • result:Error: The godot error. If this is not OK, then something went wrong with the connection
  • response_code:GeminiRequest.RESPONSECODES: Responsecode returned by the server. If this is anything other than GeminiRequest.RESPONSECODES.SUCCESS (20) you probably need to act.
  • message:String: Some message received from the server
    • if response_code is SUCCESS, this will be ""
    • if response_code is INPUT or INPUT_SENSITIVE, this will be the input prompt
    • if response_code is REDIRECT_TEMPORARY or REDIRECT_PERMANENT, this will be the new URL
    • if response_code is any of the error codes, this will be a user-presentable explanation of the error
    • if response_code is NONE, this will be either the error_string() of result or another custom GeminiRequest user-presentable error message
  • mime_type:String: Mime type of the returned document. For Gemtext documents this is text/gemini. For anynthing else like images this will be the officia mine type (IE image/png)
    • This is only set if response_code is SUCCESS (20). Otherwise it is null
  • body:PackedByteArray: Binary document content. If this is a gemtext page, you can extract the text by doing body.get_string_from_utf8()
    • This is only set if response_code is SUCCESS (20). Otherwise it is null

Parsing Gemtext

This addon also contains code to parse gemtext into a more usable format. You should read this if you are unfamiliar with gemtext.

GemtextParser class

Class used to turn a gemtext document string into a list of GemtextItems

Properties:

  • source:String: Gemtext source code string (set with constructor)
  • items:Array[GemtextItem]: (Partial) result of the parsing
  • collapse_text:bool: Collapse multiple lines of subsequent text into a single GemtextItem (defaults to false)
  • collapse_quotes:bool: Collapse multiple lines of subsequent quotes into a single GemtextItem (defaults to false)
  • collapse_list:bool: Collapse multiple lines of subsequent lists into a single GemtextItem (defaults to false)

Functions:

  • _init(gemtext:String): constructor
  • complete()->Array[GemtextItem]: Run until parsing finishes
  • is_done()->bool: Returns true if parsing has finished
  • get_progress()->float: Returns a % (0.0→1.0) number of how far along the parser is
  • next()->void: Parses next line of gemtext. Keep calling this until is_done() is true
  • static parse_all()->Array[GemtextItem]: Parse a string into an array of GemtextItems without having to instantiate an object

Signals:

  • done(items:Array[GemtextItem]): Parser has finished parsing
  • item_ready(item:GemtextItem): A GemtextItem has been completed. Useful for if you are timesharing with next() and want to build your scene as it's parsing.

GemtextItem resource

Resource used for all gemtext items. This is comparable to a javascript DOM element or a godot node (though not in a tree).

Properties:

  • type:String: Type of item. Available types are: "text", "block", "sub-sub-header", "sub-header", "header", "quote", "list", "link"
  • content:String: The text to display
  • extra:String: Secondary value. This is only used for "link" and "block" items
    • For "link"s, this is the URL
    • For "block"s, this is the alt text/programming language

Examples

Fetching a document & printing its contents

func _ready():
    var requester = GeminiRequest.new()
    add_child(requester)

    var result = await requester.request('gemini://geminiprotocol.net')
    if result.result == OK and result.response_code == GeminiRequest.RESPONSECODES.SUCCESS:
        print(result.body.get_string_from_utf8())
    else:
        printerr(result.message)

Handling user input requests

var my_input_node:LineEdit = $my_line_input

func _ready():
    requester = GeminiRequest.new()
    add_child(requester)

    var result = await requester.request('gemini://geminiprotocol.net')
    if result.result == OK and result.response_code in [GeminiRequest.RESPONSECODES.INPUT, GeminiRequest.RESPONSECODES.INPUT_SENSITIVE]:
        # Server requests an input prompt
        print("Server asks for ",result.message)
        # Make new url which contains the answer
        var prompt_url:String = GeminiRequest.make_input_url( result.url, my_input_node.text )
        result = await requester.request( prompt_url )
        if result.result == OK and result.response_code == GeminiRequest.RESPONSECODES.SUCCESS:
            print(result.body.get_string_from_utf8())
        else:
            printerr(result.message)

Rendering gemtext

Basic example for rendering gemtext into godot nodes. You can go much fancier than this of course, this is purely functional.

extends VBoxContainer

@export var gemtext:String = ""

var parser:GemtextParser

func _ready():
    parser = GemtextParser.new(gemtext)
    parser.item_ready.connect(_on_parser_item)

func _process(delta):
    if parser.is_done():
        set_process(false)
    # You probably want to do more than one `next()` per frame
    parser.next()

func _on_parser_item(item:GemtextItem):
    var out:Control
    match item.type:
        "block":
            out = CodeEdit.new()
            out.text = item.content
        "link":
            out = LinkButton.new()
            out.url = item.extra
            out.text = item.content
        _:
            out = Label.new()
            out.text = item.content

    add_chil(out)

Make requests with the Gemini protocol (https://geminiprotocol.net/) & easily parse gemtext

Reviews

0 ratings

Your Rating

Headline must be at least 3 characters but not more than 50
Review must be at least 5 characters but not more than 500
Please sign in to add a review

Quick Information

0 ratings
Gemini Protocol icon image
mocha
Gemini Protocol

Make requests with the Gemini protocol (https://geminiprotocol.net/) & easily parse gemtext

Supported Engine Version
4.0
Version String
1.0.0
License Version
MIT
Support Level
community
Modified Date
2 months ago
Git URL
Issue URL

Open Source

Released under the AGPLv3 license

Plug and Play

Browse assets directly from Godot

Community Driven

Created by developers for developers