A => .bundle/config +2 -0
@@ 1,2 @@
+---
+BUNDLE_PATH: "vendor/bundle"
A => .gitignore +9 -0
@@ 1,9 @@
+Gemfile.lock
+vendor/
+
+database.db
+keys.txt
+reports.txt
+
+views/index.haml
+views/moderation.haml
A => Gemfile +5 -0
@@ 1,5 @@
+source "https://rubygems.org"
+gem "sinatra"
+gem "puma"
+gem "haml"
+gem "sqlite3"
A => README.md +58 -0
@@ 1,58 @@
+# fclass
+
+**fclass** is a free and open-source software system for classified
+advertisements boards (think [craigslist](https://craigslist.org)), emphasizing
+anonymity, simplicity, and good moderation. It is primarily aimed at small,
+local communities.
+
+This is currently in early alpha; please do not deploy it in production.
+
+## Dependencies
+
+- Any C compiler
+- SQLite
+- Ruby
+- Bundler
+
+## Setup
+
+```
+$ bundle install
+$ bundle exec ruby scripts/migrate-v1.rb
+```
+
+## Usage
+
+First, set the `NAME` variable to the name of your page:
+
+```
+$ export NAME='example'
+```
+
+Then, create the following pages:
+
+- `views/index.haml` — homepage
+- `views/moderation.haml` — moderation log
+
+They are in the [HAML](https://haml.info/) format. Since they inherit from
+`views/layout.haml`, you do not have to write a full HTML document; just
+specify the body.
+
+Next, make a list of allowed keys in `keys.txt`, one key per line. Keys should
+be distributed to those who have permission to post on your page, and should be
+kept secret.
+
+Finally, run the server:
+
+```
+$ bundle exec ruby server.rb
+```
+
+## Miscellaneous
+
+The main database is in `database.db` and uses the SQLite format. You can
+inspect it with `sqlite3(1)`, for example.
+
+User-submitted reports are in `reports.txt`. Each line is a separate report,
+and each report consists of the ID of the post in question and then the report
+message, separated by a colon and a space.
A => magic.rb +7 -0
@@ 1,7 @@
+# long live Ruby
+
+class String
+ def truncate(chars)
+ length > chars ? self[0...chars] : self
+ end
+end
A => public/index.css +38 -0
@@ 1,38 @@
+body {
+ font-family: sans-serif;
+}
+
+nav {
+ padding: 0.5em;
+ border: 1px solid black;
+}
+
+form.search {
+ display: inline-block;
+ float: right;
+ margin: 0;
+}
+
+form.post *, form.report * {
+ display: block;
+ width: 100%;
+ margin-bottom: 1em;
+}
+
+form.post label, form.report label {
+ font-weight: bold;
+}
+
+table.metadata {
+ margin-top: 1em;
+ padding: 0.5em;
+ border: 1px solid black;
+}
+
+table.metadata th {
+ text-align: right;
+}
+
+p.details {
+ white-space: pre-wrap;
+}
A => scripts/migrate-v1.rb +13 -0
@@ 1,13 @@
+require "sqlite3"
+
+db = SQLite3::Database.new "database.db"
+
+db.execute <<EOF
+CREATE TABLE posts (
+ id INTEGER PRIMARY KEY,
+ title TEXT NOT NULL,
+ details TEXT NOT NULL,
+ location TEXT NOT NULL,
+ date TEXT NOT NULL
+);
+EOF
A => server.rb +77 -0
@@ 1,77 @@
+require "date"
+require "sinatra"
+require "sqlite3"
+
+require_relative "magic"
+
+db = SQLite3::Database.new "database.db"
+db.results_as_hash = true
+
+queries = {
+ :post => db.prepare("INSERT INTO posts (title, details, location, date) "\
+ "VALUES (?, ?, ?, ?);"),
+
+ :p => db.prepare("SELECT * FROM posts WHERE id = ?;"),
+
+ :search => db.prepare("SELECT * FROM posts WHERE "\
+ "title LIKE ? OR details LIKE ? OR location LIKE ? OR date LIKE ?;")
+}
+
+get "/" do
+ haml :index
+end
+
+get "/moderation" do
+ haml :moderation
+end
+
+get "/post" do
+ haml :post
+end
+
+post "/post" do
+ key_valid = File.foreach("keys.txt").detect do |line|
+ line.include? params[:key]
+ end
+ if not key_valid
+ halt 403, haml(:key_invalid)
+ end
+
+ queries[:post].execute(
+ params[:title],
+ params[:details],
+ params[:location],
+ Time.now.utc.iso8601
+ )
+
+ redirect "/p/#{db.last_insert_row_id}"
+end
+
+get "/p/:id" do |id|
+ p = queries[:p].execute(id).next
+ haml :p, :locals => p
+end
+
+get "/report/:id" do |id|
+ haml :report
+end
+
+post "/report/:id" do |id|
+ Thread.new do
+ File.open("reports.txt", "a") do |f|
+ f.write("#{id}: #{params[:report]}\n")
+ end
+ end
+
+ haml :report_submitted
+end
+
+get "/search" do
+ query = "%#{params[:q].gsub /\s+/, "%"}%"
+ res = queries[:search].execute(query)
+ haml :search, :locals => {
+ :q => params[:q],
+ :res => res
+ }
+end
+
A => views/key_invalid.haml +6 -0
@@ 1,6 @@
+%h1 posting forbidden
+%p
+ sorry mate. you have either mistyped your key, or your key has been revoked by
+ the administrators (see the
+ %a{:href => "/moderation"}> moderation log
+ ).
A => views/layout.haml +19 -0
@@ 1,19 @@
+%html
+ %head
+ %title= ENV["NAME"]
+ %link{:rel => "stylesheet", :type => "text/css", :href => "/index.css"}
+ %body
+ %nav
+ %a{:href => "/"}= ENV["NAME"]
+ |
+ %a{:href => "/post"} submit new post
+ |
+ %a{:href => "/moderation"} moderation log
+ %form.search{:action => "/search"}
+ %input{:name => "q", :type => "text", :placeholder => "search…"}
+ %input{:type => "submit", :value => "go!"}
+ != yield
+ %footer
+ %hr
+ %i
+ fclass v1
A => views/p.haml +13 -0
@@ 1,13 @@
+%h1= title
+%table.metadata
+ %tr
+ %th posted on
+ %td= date
+ %tr
+ %th location
+ %td= location
+ %tr
+ %th
+ %td
+ %a{:href => "/report/#{id}"} report fraud or spam
+%p.details= details
A => views/post.haml +15 -0
@@ 1,15 @@
+%h1 submit new post
+%form.post{:method => "post"}
+ %label{:for => "key"} key
+ %input#key{:name => "key", :type => "text", :required => true}
+
+ %label{:for => "title"} title
+ %input#title{:name => "title", :type => "text", :required => true}
+
+ %label{:for => "details"} details
+ %textarea#details{:name => "details", :rows => "12", :required => true}
+
+ %label{:for => "location"} location
+ %input#location{:name => "location", :type => "text", :required => true}
+
+ %input{:type => "submit", :value => "go!"}
A => views/report.haml +9 -0
@@ 1,9 @@
+%h1 report fraud or spam
+%form.report{:method => "post"}
+ %label{:for => "report"} details
+ %i
+ please provide as much evidence as possible — the more you provide, the more
+ likely the post will be taken down.
+ %textarea#report{:name => "report", :rows => "12", :required => true}
+
+ %input{:type => "submit", :value => "report!"}
A => views/report_submitted.haml +6 -0
@@ 1,6 @@
+%h1 thank you for your report!
+%p
+ we will look into it as soon as possible. once the report is handled, the
+ result will be displayed in the
+ %a{:href => "/moderation"}> moderation log
+ \.
A => views/search.haml +12 -0
@@ 1,12 @@
+%h1
+ search results for “
+ %i>= q
+ ”
+%table.search
+ - res.each do |post|
+ %tr
+ %td.date= post["date"]
+ %th.title
+ %a{:href => "/p/#{post["id"]}"}= post["title"]
+ %td.details= post["details"].truncate 100
+ %td.location= post["location"]