diff --git a/.gitignore b/.gitignore

new file mode 100644

index 0000000000000000000000000000000000000000..7b724693cb1228ac01c4054b648888b95c32cace

--- /dev/null

+++ b/.gitignore

@@ -0,0 +1,2 @@

+data/data.sqlite

+gmnifaq.conf

diff --git a/LICENSE b/LICENSE

new file mode 100755

index 0000000000000000000000000000000000000000..027d71d0073f90db1ea80f7524f6fceccb001e93

--- /dev/null

+++ b/LICENSE

@@ -0,0 +1,29 @@

+BSD 3-Clause License

+

+Copyright (c) 2018-2020, René Wagner

+All rights reserved.

+

+Redistribution and use in source and binary forms, with or without

+modification, are permitted provided that the following conditions are met:

+

+* Redistributions of source code must retain the above copyright notice, this

+ list of conditions and the following disclaimer.

+

+* Redistributions in binary form must reproduce the above copyright notice,

+ this list of conditions and the following disclaimer in the documentation

+ and/or other materials provided with the distribution.

+

+* Neither the name of the copyright holder nor the names of its

+ contributors may be used to endorse or promote products derived from

+ this software without specific prior written permission.

+

+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"

+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE

+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE

+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE

+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL

+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR

+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER

+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,

+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE

+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

diff --git a/README.md b/README.md

new file mode 100644

index 0000000000000000000000000000000000000000..a62cea7fb42d100a02f22b60bc623d0f2e965aca

--- /dev/null

+++ b/README.md

@@ -0,0 +1,17 @@

+# gmnifaq

+

+gmnifaq is going to be a simple, self-hostable FAQ-engine for the [gemini protocol](gemini://gemini.circumlunar.space)

+

+## planned features

+

+- support for tags

+- searching

+- view by tag

+- view all

+

+## requirements

+

+- gemini server with cgi enabled

+- Perl >= 5.28

+- SQLite

+

diff --git a/data/data.sqlite.example b/data/data.sqlite.example

new file mode 100644

index 0000000000000000000000000000000000000000..d8c4059aa1104544e7399cafb62f38c9b210290e

Binary files /dev/null and b/data/data.sqlite.example differ

diff --git a/gmnifaq.conf.example b/gmnifaq.conf.example

new file mode 100644

index 0000000000000000000000000000000000000000..2c0c2be11d38c80d85179ba3d324c1437390c9d5

--- /dev/null

+++ b/gmnifaq.conf.example

@@ -0,0 +1,2 @@

+$sitename = 'gemini faq';

+$siteintro = 'Welcome to gmnifaq, the simple cgi engine for your gemini capsule';

diff --git a/index.pl b/index.pl

new file mode 100755

index 0000000000000000000000000000000000000000..d7865c082337d027e890cf73f9ebb8fbc7c91d58

--- /dev/null

+++ b/index.pl

@@ -0,0 +1,97 @@

+#!/usr/bin/perl

+# Copyright René Wagner 2020

+# licenced under BSD 3-Clause licence

+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo

+

+use strict;

+use DBI;

+

+# define return codes

+our %RC = (

+ 'INPUT', 10,

+ 'SENSITIVE_INPUT', 11,

+ 'SUCCESS', 20,

+ 'TEMPORARY_REDIRECT', 30,

+ 'PERMANENT_REDIRECT', 31,

+ 'TEMPORARY_FAILURE', 40,

+ 'SERVER_UNAVAILABLE', 41,

+ 'CGI_ERROR', 42,

+ 'PROXY_ERROR', 43,

+ 'SLOW_DOWN', 44,

+ 'PERMANENT_FAILURE', 50,

+ 'NOT_FOUND', 51,

+ 'GONE', 52,

+ 'PROXY_REQUEST_REFUSE', 53,

+ 'BAD_REQUEST', 59,

+ 'CLIENT_CERT_REQUIRED', 60,

+ 'CERT_NOT_AUTHORISED', 61,

+ 'CERT_NOT_VALID', 62

+);

+

+my $sitename = 'gmnifaq';

+my $siteintro = 'Welcome to gmnifaq!';

+

+my $dsn = "DBI:SQLite:dbname=data/data.sqlite";

+

+# enable UTF-8 mode for everything

+use utf8;

+binmode STDOUT, ':utf8';

+binmode STDERR, ':utf8';

+

+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI')

+{

+ write_response('CGI_ERROR', 'CGI execution error', undef);

+}

+

+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; }

+

+if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) };

+

+my @body = ();

+push @body, '# Welcome to '. $sitename;

+push @body, '';

+push @body, $siteintro;

+push @body, '';

+push @body, header();

+push @body, '';

+push @body, '## Meta';

+push @body, '';

+push @body, 'Search';

+push @body, '=> tags.pl Tags';

+push @body, 'View all';

+push @body, footer();

+

+write_response('SUCCESS', 'text/gemini', @body);

+

+exit;

+

+sub header

+{

+ my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr;

+

+ my $tagcount = $dbh->selectrow_array("SELECT count(id) from tags");

+ my $faqcount = $dbh->selectrow_array("SELECT count(id) from questions");

+ $dbh->disconnect();

+

+ return sprintf('We are currently serving %d FAQs categorized with %d tags!', $faqcount, $tagcount);

+}

+

+sub footer

+{

+ return ('', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq');

+}

+

+sub write_response

+{

+ my ($returncode, $meta, @content) = @_;

+

+ if (!defined($RC{$returncode})) { die "Unknown response code!"; }

+

+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta);

+ foreach (@content)

+ {

+ print("$_\r\n");

+ }

+

+ exit;

+}

diff --git a/tags.pl b/tags.pl

new file mode 100755

index 0000000000000000000000000000000000000000..87405788d7c7506edf9d023c2b199d06fbf64030

--- /dev/null

+++ b/tags.pl

@@ -0,0 +1,105 @@

+#!/usr/bin/perl

+# Copyright René Wagner 2020

+# licenced under BSD 3-Clause licence

+# https://git.sr.ht/~rwa/gmni-perl-cgi-demo

+

+use strict;

+use DBI;

+#use URI::Encode qw(uri_encode uri_decode);

+# define return codes

+our %RC = (

+ 'INPUT', 10,

+ 'SENSITIVE_INPUT', 11,

+ 'SUCCESS', 20,

+ 'TEMPORARY_REDIRECT', 30,

+ 'PERMANENT_REDIRECT', 31,

+ 'TEMPORARY_FAILURE', 40,

+ 'SERVER_UNAVAILABLE', 41,

+ 'CGI_ERROR', 42,

+ 'PROXY_ERROR', 43,

+ 'SLOW_DOWN', 44,

+ 'PERMANENT_FAILURE', 50,

+ 'NOT_FOUND', 51,

+ 'GONE', 52,

+ 'PROXY_REQUEST_REFUSE', 53,

+ 'BAD_REQUEST', 59,

+ 'CLIENT_CERT_REQUIRED', 60,

+ 'CERT_NOT_AUTHORISED', 61,

+ 'CERT_NOT_VALID', 62

+);

+

+my $sitename;

+my $siteintro;

+

+my $dsn = "DBI:SQLite:dbname=data/data.sqlite";

+

+# enable UTF-8 mode for everything

+use utf8;

+binmode STDOUT, ':utf8';

+binmode STDERR, ':utf8';

+

+if (!defined($ENV{'SERVER_PROTOCOL'}) || $ENV{'SERVER_PROTOCOL'} ne 'GEMINI')

+{

+ write_response('CGI_ERROR', 'CGI execution error', undef);

+}

+

+if ( -f './gmnifaq.conf' ) { do './gmnifaq.conf'; }

+

+if ( !-f 'data/data.sqlite' ) { write_response('PERMANENT_FAILURE', 'Permanent failure', undef) };

+

+my @body = ();

+push @body, '# Welcome to '. $sitename;

+push @body, '';

+push @body, 'Select a tag to browse the questions associated with this tag.';

+push @body, '';

+push @body, '## Tags';

+push @body, '';

+push @body, tags();

+push @body, footer();

+

+write_response('SUCCESS', 'text/gemini', @body);

+

+exit;

+

+sub tags

+{

+ my $dbh = DBI->connect($dsn, '', '', { RaiseError => 1 }) or die $DBI::errstr;

+

+ my @tags;

+ my $stmt = $dbh->prepare('SELECT name, count(t_id) FROM tags LEFT JOIN tags_questions ON tags_questions.t_id = tags.id GROUP BY t_id');

+ $stmt->execute();

+

+ my $rows = $stmt->fetchall_arrayref;

+ $dbh->disconnect();

+

+ if ( !scalar @$rows ) {

+ push @body, 'No tags found!';

+ }

+ else {

+ foreach (@$rows)

+ {

+ push @tags, sprintf("=> tags.pl?%s %s (%d entrys)", @$_[0], @$_[0], @$_[1]);

+ }

+ }

+ return @tags;

+}

+

+sub footer

+{

+ return ('', '=> index.pl [Home]', '=> https://git.sr.ht/~rwa/gmnifaq powered by gmnifaq');

+}

+

+sub write_response

+{

+ my ($returncode, $meta, @content) = @_;

+

+ if (!defined($RC{$returncode})) { die "Unknown response code!"; }

+

+ printf("%d %s\r\n", $RC{$returncode}, ($meta eq '') ? $returncode : $meta);

+ foreach (@content)

+ {

+ print("$_\r\n");

+ }

+

+ exit;

+}