~jhpotter/fclass

aaaf06a8d9d645698c3ed6ac74ece4d2256af7c3 — Jeremy Potter 3 months ago master
Initial commit
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&nbsp;
  %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&nbsp;
  %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"]