From: Alexis Laferrière Date: Thu, 24 Jul 2014 00:40:19 +0000 (-0400) Subject: contrib: intro of tnitter X-Git-Tag: v0.6.7~3^2 X-Git-Url: http://nitlanguage.org?ds=sidebyside contrib: intro of tnitter Signed-off-by: Alexis Laferrière --- diff --git a/contrib/tnitter/Makefile b/contrib/tnitter/Makefile new file mode 100644 index 0000000..c2a1d4a --- /dev/null +++ b/contrib/tnitter/Makefile @@ -0,0 +1,3 @@ +all: + mkdir -p bin/ + ../../bin/nitg --dir bin src/tnitter.nit diff --git a/contrib/tnitter/README.md b/contrib/tnitter/README.md new file mode 100644 index 0000000..0eca29d --- /dev/null +++ b/contrib/tnitter/README.md @@ -0,0 +1,23 @@ +Tnitter is a Twitter-like micro-blogging platform + +# Compile and execute + +Make sure all the required packages are installed with: `apt-get install libevent-dev libsqlite3-dev` + +To compile, run: `make` + +To execute, run: `bin/tnitter` + +The Web interface will be accessible at http://localhost:8080/ + +# Main server + +The Tnitter application is deployed with other `nitcorn` projects at http://tnitter.xymus.net/ + +# Notable implementation details + +* Implemented in Nit using the `nitcorn` framework. +* On the server side, besides `nitcorn` it uses the Nit modules `sqlite3`, `md5` and `privileges` +* The client-side UI is implemented with bootstrap 3.0 and jquery 1.11 +* Passwords are salted and hashed, but sent in clear text to the server +* Launches on localhost on port 80 if running as root, on 8080 otherwise diff --git a/contrib/tnitter/src/action.nit b/contrib/tnitter/src/action.nit new file mode 100644 index 0000000..db3847f --- /dev/null +++ b/contrib/tnitter/src/action.nit @@ -0,0 +1,232 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014 Alexis Laferrière +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# View and controller of Tnitter +module action + +import nitcorn + +import model + +redef class Session + # User logged in + var user: nullable String = null +end + +# Main Tnitter Action +class Tnitter + super Action + + var db_path = "tnitter.db" + var db = new DB.open(db_path) + + # Header on pages served by this `Action` + # + # Keywords to `Text::replace`: + # * `%app_path%` is the main URL to reach this `Action` + # * `%nav_right%` is the pulled right part of the header, used for login form + var header = """ +""" + + # Template of the pages served by this `Action` + # + # Keywords to `Text::replace`: + # * The `%header%`, first thing in the `` + # * The main page `%content%` within a `
` + var template = """ + + + + Tnitter + + + + + + + + + + +%header% + +
+ %content% +
+ +""" + + redef fun answer(request, turi) + do + # Get existing session + var session = request.session + + # Error to display on page as a dismissable panel + var error = null + + # Login/logout + if turi == "/login" and request.post_args.keys.has("user") and + request.post_args.keys.has("pass") then + + var user = request.post_args["user"].trim + var pass = request.post_args["pass"] + + var original_user = db.check_login(user, pass) + if original_user != null then + # Log in successful + if session == null then session = new Session + session.user = original_user + else + # Check for basic requirements + if user.is_empty then + error = "Username must have at least 1 character" + else if user.chars.has(' ') or user.chars.has('\n') then + error = "Username cannot contain white spaces" + else if db.sign_up(user, pass) then + # Sign up successful + if session == null then session = new Session + session.user = user + else + # Invalid user/pass + error = "Invalid combination of username and password" + session = null + end + end + else if turi == "/logout" then + # Logging out + session = null + else if turi == "/post" and request.post_args.keys.has("text") and session != null then + var user = session.user + if user != null then + # Post a Tnit! + var text = request.post_args["text"] + db.post(user, text) + end + end + + var login_or_out + var content + if session == null or session.user == null then + # Log in form in the navbar + login_or_out = """ +
  • + +
  • + """ + + # Cannot post when not logged in + content = "" + else + # Log out form in the navbar + login_or_out = """ +
  • +
  • + +
  • + """ + + # Post form + content = """ +
    +
    +
    +
    Share your thoughts
    + + + + +
    +
    +
    + """ + end + + # Show error if any + var error_html + if error != null then + error_html = """ + + """ + else error_html = "" + + # Load the last 16 Tnits + var posts = db.latest_posts(16) + + var html_posts = new Array[String] + for post in posts do + html_posts.add "@{post.user.html_escape}{post.text.html_escape}" + end + + content += """ +
    +
    Latest Tnits
    + + {{{html_posts.join("\n")}}} +
    +
    +
    + """ + + # Get page from template, we replace the header first so we can replace + # everything on the same body afterwards + var body = template. + replace("%header%", header). + replace("%app_path%", request.uri.strip_extension(turi) + "/"). + replace("%header_right%", login_or_out). + replace("%content%", error_html + content) + + # Build response + var response = new HttpResponse(200) + response.body = body + response.session = session + return response + end +end diff --git a/contrib/tnitter/src/model.nit b/contrib/tnitter/src/model.nit new file mode 100644 index 0000000..bb46193 --- /dev/null +++ b/contrib/tnitter/src/model.nit @@ -0,0 +1,113 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014 Alexis Laferrière +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Data and DB model of Tnitter +module model + +import sqlite3 +import md5 + +# The Tnitter database +class DB + super Sqlite3DB + + redef init open(path) + do + super + create_tables + end + + # Create the needed tables + fun create_tables + do + assert create_table("IF NOT EXISTS users (user TEXT PRIMARY KEY, pass TEXT)") else + print error or else "?" + end + + assert create_table("IF NOT EXISTS posts (user TEXT, text TEXT, posted DATETIME DEFAULT CURRENT_TIMESTAMP)") else + print error or else "?" + end + end + + # Check if the login credentials are valid + # + # If valid, returns the username at database creation time. Otherwise returns `null`. + fun check_login(user, pass: String): nullable String + do + var stmt = select("user FROM users WHERE lower({user.to_sql_string}) = lower(user) " + + "AND {pass.tnitter_hash.to_sql_string} = pass") + assert stmt != null else print error or else "?" + + for row in stmt do return row[0].to_s + return null + end + + # Try to sign up a new user, return `true` on success + fun sign_up(user, pass: String): Bool + do + # Check if already in user + var stmt = select("user FROM users WHERE lower({user.to_sql_string}) = lower(user)") + assert stmt != null else print error or else "?" + + if not stmt.iterator.to_a.is_empty then return false + + # Insert intro BD + assert insert("INTO users(user, pass) VALUES ({user.to_sql_string}, {pass.tnitter_hash.to_sql_string})") else + print error or else "?" + end + + return true + end + + # Tnit something + fun post(user, text: String) + do + assert insert("INTO posts(user, text) VALUES ({user.to_sql_string}, {text.to_sql_string})") else + print error or else "?" + end + end + + # Get the latest tnits + fun latest_posts(count: Int): Array[Post] + do + var stmt = select("user, text FROM posts ORDER BY datetime(posted) DESC LIMIT {count}") + assert stmt != null else print error or else "?" + + var posts = new Array[Post] + for row in stmt do posts.add new Post(row[0].to_s, row[1].to_s) + + return posts + end +end + +# A single post (or Tnit) +class Post + # The author + var user: String + + # Tnit content + var text: String +end + +redef class String + # Hash passwords for Tnitter + fun tnitter_hash: String do return (self+salt).md5 +end + +# Salt used on passwords +# +# Can be redefed by user modules +fun salt: String do return "tnitter is cool" diff --git a/contrib/tnitter/src/tnitter.nit b/contrib/tnitter/src/tnitter.nit new file mode 100644 index 0000000..490fc07 --- /dev/null +++ b/contrib/tnitter/src/tnitter.nit @@ -0,0 +1,65 @@ +# This file is part of NIT ( http://www.nitlanguage.org ). +# +# Copyright 2014 Alexis Laferrière +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Web server part of the Tnitter project +module tnitter + +import privileges + +import model +import action + +redef class OptionContext + var drop = new OptionUserAndGroup.for_dropping_privileges + var help = new OptionBool("Print this message", "--help", "-h") + + init do add_option(drop, help) +end + +# Avoid executing when running tests +if "NIT_TESTING".environ == "true" then exit 0 + +# Prepare options +var opts = new OptionContext +opts.parse(args) +if not opts.errors.is_empty or opts.help.value then + print opts.errors + print "Usage: tnitter [Options]" + opts.usage + exit 1 +end + +# If we can, we use port 80 +var interfac +if sys.uid == 0 then # Are we root? + interfac = "localhost:80" +else interfac = "localhost:8080" + +# Setup server +var vh = new VirtualHost(interfac) +var factory = new HttpFactory.and_libevent +factory.config.virtual_hosts.add vh + +# Drop to a low-privileged user +var user_group = opts.drop.value +if user_group != null then user_group.drop_privileges + +# Complete server config +vh.routes.add new Route(null, new Tnitter) + +# Run +print "Launching server on http://{interfac} ..." +factory.run