Skip to content
Browse files

Merge branch 'master' into win32

  • Loading branch information...
2 parents 715f9db + b0a5fd5 commit f79e23b181c59684dcf4fcf13510332421700bb7 @ToxicFrog committed Mar 21, 2012
Showing with 3,497 additions and 837 deletions.
  1. +3 −1 README.txt
  2. +11 −3 app.lua
  3. BIN bin/categories.exe
  4. BIN bin/lfs.dll
  5. BIN bin/lua5.1.dll
  6. BIN bin/mime/core.dll
  7. BIN bin/socket/core.dll
  8. BIN bin/steam2backloggery.exe
  9. +13 −2 steam2backloggery.cfg → bin/steamtools.cfg
  10. +0 −3 bledit.sh
  11. +0 −62 bledit.wlua
  12. +0 −18 bledit/controls.lua
  13. +0 −95 bledit/fields.lua
  14. +0 −329 bledit/gui.lua
  15. +0 −175 bledit/window.lua
  16. +4 −2 categories.lua
  17. +43 −0 config.lua
  18. BIN images/1_5stars.bmp
  19. BIN images/2_5stars.bmp
  20. BIN images/3_5stars.bmp
  21. BIN images/4_5stars.bmp
  22. BIN images/5_5stars.bmp
  23. BIN images/beaten.bmp
  24. BIN images/completed.bmp
  25. BIN images/delete.bmp
  26. BIN images/edit.bmp
  27. BIN images/mastered.bmp
  28. BIN images/nowplaying.bmp
  29. BIN images/null.bmp
  30. BIN images/own_borrow.bmp
  31. BIN images/own_ghost.bmp
  32. BIN images/own_other.bmp
  33. BIN images/own_owned.bmp
  34. BIN images/unfinished.bmp
  35. BIN images/wishlist.bmp
  36. +292 −0 lib/ltn12.lua
  37. +87 −0 lib/mime.lua
  38. +133 −0 lib/socket.lua
  39. +281 −0 lib/socket/ftp.lua
  40. +350 −0 lib/socket/http.lua
  41. +251 −0 lib/socket/smtp.lua
  42. +123 −0 lib/socket/tp.lua
  43. +297 −0 lib/socket/url.lua
  44. +5 −2 libbl.lua
  45. +5 −52 libsteam.lua
  46. +83 −0 libsteam/games.lua
  47. +5 −0 mkwin32.sh
  48. +95 −89 steam2backloggery.lua
  49. +54 −0 steamtools.cfg
  50. +4 −4 txt2backloggery.lua
  51. +171 −0 xml/DOMHandler.lua
  52. +38 −0 xml/README
  53. +51 −0 xml/easyDOMHandler.lua
  54. +305 −0 xml/handler.lua
  55. +154 −0 xml/printHandler.lua
  56. +167 −0 xml/simpleTreeHandler.lua
  57. +472 −0 xml/xml.lua
View
4 README.txt
@@ -1,6 +1,8 @@
At the moment the only programs in here that're really "release-ready" are steam2backloggery and categories.
CATEGORIES
+
+Before using this program YOU MUST EXIT STEAM. If Steam is still running it will immediately overwrite any changes this tool attempts to make.
To use it, double-click on categories.lua (or, if you downloaded the windows package, categories.exe). It will prompt you for the location of your Steam install - the easiest way to do this is to drag-and- drop your steam.exe into the window and press enter.
@@ -20,7 +22,7 @@ Once it has the game lists, it will create a file listing all of the games it's
When you close the editor, it will prompt you for a Backloggery category to use, and then add entries for all of the listed games to Backloggery. It is recommended that you use "PC", "PCDL" (PC Download), or "Steam" as the category, but it won't stop you from filing them under PS2 or SNES or whatever. If you enter a category that Backloggery doesn't support, it'll re-prompt.
-There is also a configuration file, "steam2backloggery.cfg", for configuring commonly used settings. The contents of this file should be fairly self- explanatory, but here is brief list of settings you can configure:
+There is also a configuration file, "steamtools.cfg", for configuring commonly used settings. The contents of this file should be fairly self- explanatory, but here is brief list of settings you can configure:
STEAM Location of Steam.exe
USER,PASS Login information for Backloggery
View
14 app.lua
@@ -1,16 +1,24 @@
+package.path = package.path..";lib/?.lua"
+
require "util.io"
require "util.misc"
local _main = main
+local _EXIT = {}
+local os_exit = os.exit
+
+function os.exit(code)
+ _EXIT[1] = code
+ error(_EXIT)
+end
function main(...)
local r,e = va_xpcall(_main, debug.traceback, ...)
- if not r then
+ if not r and e ~= _EXIT then
io.eprintf("\n\nAn error occurred! Please report this to the developer.\n%s\n", e)
end
io.printf("\nPress enter to quit...\n")
io.read()
- os.exit(r and e or -1)
+ os.exit(_EXIT[1] or (r and e) or -1)
end
-
View
BIN bin/categories.exe
Binary file not shown.
View
BIN bin/lfs.dll
Binary file not shown.
View
BIN bin/lua5.1.dll
Binary file not shown.
View
BIN bin/mime/core.dll
Binary file not shown.
View
BIN bin/socket/core.dll
Binary file not shown.
View
BIN bin/steam2backloggery.exe
Binary file not shown.
View
15 steam2backloggery.cfg → bin/steamtools.cfg
@@ -1,4 +1,4 @@
--- configuration file for steam2backloggery
+-- configuration file for steamtools programs
-- lines starting with '--' are comments
-- anything not specified here, the user will be asked for, or a sensible
-- default will be chosen.
@@ -7,6 +7,17 @@
-- STEAM = [[C:/Program Files (x86)/Steam/Steam.exe]]
-- STEAM = [[F:\Games\Steam\Steam.exe]]
+-- Steam username. Optional, used by some programs for file names etc.
+-- STEAMNAME = [[toxicfrog]]
+
+-- Steam numeric user ID. Steamtools will try to determine this from your Steam
+-- log file; this will override it. Use if it gets it wrong or can't figure it
+-- out.
+-- Steam IDs are usually in the form X:Y:Z where X, Y, and Z are numbers. Break
+-- it down as shown in this example.
+-- shows up as 0:1:3103024 in the log file
+-- STEAMID = { 0, 1, 3103024 }
+
-- Backloggery username and password
-- USER = [[Me]]
-- PASS = [[topsecret]]
@@ -40,4 +51,4 @@ IGNORE = [[
DEBUG = {
ONE_PAGE_ONLY = false;
-}
+}
View
3 bledit.sh
@@ -1,3 +0,0 @@
-#!/bin/bash
-
-env LD_LIBRARY_PATH=iup lua -e 'LINUX=true' bledit.wlua
View
62 bledit.wlua
@@ -1,62 +0,0 @@
-package.cpath = package.cpath..";iup/lib?51.so"
-
-require "iuplua"
-local gui = require "bledit.gui"
-
-local GAMES,COOKIE
-
-bledit = {}
-
-function bledit.games()
- return GAMES
-end
-
-function bledit.cookie()
- return COOKIE
-end
-
-function bledit.loadGames()
- COOKIE._games = nil -- delete cache to force a reload
- GAMES = COOKIE:games()
- gui.listGames(GAMES, "_console_str", "name")
-end
-
-function main(...)
- gui.init()
-
- -- load config file
- if loadfile("steam2backloggery.cfg") then
- local r,e = xpcall(loadfile("steam2backloggery.cfg"), debug.traceback)
- if not r then
- gui.warn("Warning", "Found config file steam2backloggery.cfg, but couldn't load it:\n"..e)
- end
- end
-
- -- log in to backloggery
- local bl = require "libbl"
- local cookie
-
- while not cookie do
- local status,user,pass = gui.getLogin(USER, PASS)
- if not status then os.exit(1) end
-
- cookie,status = bl.login(user,pass)
- if not cookie then
- gui.warn("Error", "Login failed: "..status)
- end
- end
-
- COOKIE = cookie
- gui.status("Logged in as "..cookie.user)
- bledit.loadGames()
-
- gui.main()
-end
-
-local r,e = xpcall(main, debug.traceback)
-if not r then
- gui.warn("Error", tostring(e))
-end
-
-iup.Close()
-os.exit(0)
View
18 bledit/controls.lua
@@ -1,18 +0,0 @@
--- the set of all user controls not used for editing game data, as well as
--- feedback controls
-local controls = {
- status = iup.label { title = "Not Logged In" };
- new = iup.button { title = "New"; padding = "4x"; active = "NO" };
- edit = iup.button { title = "Edit"; padding = "4x" };
- delete = iup.button { title = "Delete"; padding = "4x" };
- save = iup.button { title = "Save"; padding = "4x" };
- gamelist = iup.tree {
- expand = "YES";
- size = "200x";
- maxsize = "200x";
- markmode = "MULTIPLE";
- };
- nrof_selected = 0;
-};
-
-return controls
View
95 bledit/fields.lua
@@ -1,95 +0,0 @@
--- create a dropdown for selecting a gaming system from the list of systems
--- supported by backloggery
-local function SystemSelector()
- local bl = require "libbl"
-
- local systems = {}
-
- for short,long in pairs(bl.platforms) do
- table.insert(systems, long)
- end
-
- table.sort(systems)
- systems.dropdown = "YES";
-
- return iup.list(systems)
-end
-
--- create a dropdown for selecting a region from the list of regions supported
--- by backloggery
-local function RegionSelector()
- return iup.list {
- dropdown = "YES";
-
- "Brazil";
- "China";
- "Japan";
- "Korea";
- "N.Amer";
- "PAL";
- }
-end
-
-local function toggle(text, image, both)
- if LINUX and both then
- return iup.hbox {
- alignment = "ACENTER";
-
- iup.toggle { image = image; };
- iup.label { title = text; };
- };
- elseif LINUX then
- return iup.toggle { image = image; }
- else
- return iup.toggle { title = text; }
- end
-end
-
---------------------------------------------------------------------------------
--- internal widget banks
---------------------------------------------------------------------------------
-
--- the set of all editing fields
-local fields = {
- -- game information
- title = iup.text { expand = "HORIZONTAL" };
- compilation = iup.text { expand = "HORIZONTAL" };
- system = SystemSelector();
- original_system = SystemSelector();
- region = RegionSelector();
- ownership = {
- toggle("Owned", "images/own_owned.bmp", true);
- toggle("Formerly Owned", "images/own_ghost.bmp", true);
- toggle("Borrowed/Rented", "images/own_borrow.bmp", true);
- toggle("Other", "images/own_other.bmp", true);
- };
-
- -- progress
- status = {
- toggle("Unfinished", "images/unfinished.bmp", true);
- toggle("Beaten", "images/beaten.bmp", true);
- toggle("Completed", "images/completed.bmp", true);
- toggle("Mastered", "images/mastered.bmp", true);
- toggle("Null", "images/null.bmp", true);
- };
- achievements = iup.text { size = "30x"; spin = "YES"; spinmax = 999; };
- max_achievements = iup.text { size = "30x"; spin = "YES"; spinmax = 999; };
- notes = iup.text { expand = "HORIZONTAL" };
- online_info = iup.text { expand = "HORIZONTAL" };
-
- -- review
- rating = {
- toggle("5 Stars", "images/5_5stars.bmp");
- toggle("4 Stars", "images/4_5stars.bmp");
- toggle("3 Stars", "images/3_5stars.bmp");
- toggle("2 Stars", "images/2_5stars.bmp");
- toggle("1 Star", "images/1_5stars.bmp");
- iup.toggle { title = "No Rating" };
-
- readmap = { 5, 4, 3, 2, 1, 0 };
- writemap = { [0] = 6, 5, 4, 3, 2, 1 };
- };
- comments = iup.text { expand = "YES"; multiline = "YES"; wordwrap = "YES" };
-}
-
-return fields
View
329 bledit/gui.lua
@@ -1,329 +0,0 @@
-require "iuplua"
-require "util.table"
-
-local gui = {}
-
---------------------------------------------------------------------------------
--- internal widgets and whatnot
---------------------------------------------------------------------------------
-
-local fields = require "bledit.fields"
-local controls = require "bledit.controls"
-local win = require "bledit.window"
-
---------------------------------------------------------------------------------
--- callbacks
---------------------------------------------------------------------------------
-
-function controls.delete:action()
- -- get list of selected games
- local selected = controls.gamelist.markednodes
-
- -- mark them for delete
- for i=1,#selected do
- if selected:sub(i,i) == "+" then
- local game = iup.TreeGetUserId(controls.gamelist, i-1)
- if game then
- game._delete = true
- end
- end
- end
-
- -- redisplay tree
- -- where do we get the master game list from?
- gui.listGames(bledit.games(), "_console_str", "name")
-
- -- restore list of selected nodes
- controls.gamelist.markednodes = selected
-end
-
-function controls.edit:action()
- -- get list of selected games
- local selected = controls.gamelist.markednodes
-
- -- mark them for edit
- for i=1,#selected do
- if selected:sub(i,i) == "+" then
- local game = iup.TreeGetUserId(controls.gamelist, i-1)
- if game then
- bledit.cookie():details(game.id)
- game._dirty = true
- end
- end
- end
-
- -- redisplay tree
- gui.listGames(bledit.games(), "_console_str", "name")
-
- -- restore list of selected nodes
- controls.gamelist.value = selected:find("+") and selected:find("+")-1 or "ROOT"
- controls.gamelist.markednodes = selected
-
- gui.loadFields(fields.game)
-end
-
-function controls.save:action()
- local changed,deleted,unchanged = 0,0,0
-
- if fields.game and fields.game._dirty then
- gui.saveFields(fields.game)
- end
-
- table.foreach(bledit.games(), function(_, game)
- if game._delete then
- deleted = deleted+1
- elseif game._dirty then
- changed = changed+1
- else
- unchanged = unchanged+1
- end
- end)
-
- if changed + deleted == 0 then
- iup.Alarm("Nothing to save!", "You must edit or delete some games before you can save.", "OK")
- return
- end
-
- if 1 ~= iup.Alarm("Save changes?",
- "About to make the following changes to your Backloggery account:\n"
- .." "..deleted.." games will be deleted entirely\n"
- .." "..changed.." games will have their information edited\n"
- .." "..unchanged.." games will be left untouched.\nProceed?",
- "OK", "Cancel")
- then
- return
- end
-
- table.foreach(bledit.games(), function(_, game)
- if game._delete then
- print("delete", game.name, bledit.cookie():deletegame(game.id))
-
- elseif game._dirty then
- game._dirty = nil
- print("edit", game.name)
- bledit.cookie():editgame(game.id)
- end
- end)
-
- gui.listGames(bledit.games(), "_console_str", "name")
-end
-
-function controls.gamelist:selection_cb(id, status)
- local game = iup.TreeGetUserId(controls.gamelist, id)
- if not game then
- -- there's no game associated with this id
- elseif status == 1 then
- -- we selected a new game - load it into the fields
- -- if we were editing another game, save that one first
- if fields.game and fields.game._dirty then
- gui.saveFields(fields.game)
- end
-
- gui.loadFields(game)
- controls.nrof_selected = controls.nrof_selected +1
- else
- -- we deselected a game - do nothing
- controls.nrof_selected = controls.nrof_selected -1
- gui.editable(false)
- end
-end
-
---------------------------------------------------------------------------------
--- public API
---------------------------------------------------------------------------------
-
-function gui.init()
- win:show()
- controls.editpane = iup.GetDialogChild(win, "editpane")
-end
-
-function gui.warn(title, message)
- iup.Alarm(title, message, "OK")
-end
-
-function gui.getLogin(USER,PASS)
- return iup.GetParam("Backloggery Login", nil, "Username: %s\nPassword:%s\n", USER or "", PASS or "")
-end
-
-function gui.status(status)
- controls.status.title = status
-end
-
-function gui.main()
- -- ok, now we can edit things
- win.active = "YES"
- iup.MainLoop()
-end
-
-function gui.listGames(allgames, group_by, sort_by)
- local function getNodeImage(game)
- if game._dirty then
- return "images/edit.bmp"
- elseif game._delete then
- return "images/delete.bmp"
- elseif game.playing then
- return "images/nowplaying.bmp"
- elseif game.wishlist then
- return "images/wishlist.bmp"
- else
- return "images/"..game._complete_str..".bmp"
- end
- end
-
- -- due to the way tree construction works, we need to do the categories
- -- in order, but the invidual games in reverse order - branches are added
- -- top-down, leaves bottom-up
- local function sort(x, y)
- if x[group_by] == y[group_by] then
- return x[sort_by] > y[sort_by]
- else
- return x[group_by] < y[group_by]
- end
- end
-
- local games = {}; for _,game in pairs(allgames) do table.insert(games, game) end
- table.sort(games, sort)
-
- controls.gamelist.delnode = "ALL"
-
- --local branches = { [games[1][group_by]] = true }
- --controls.gamelist.addbranch = games[1][group_by]
- local branches = {}
-
- for i,game in ipairs(games) do
- if not branches[game[group_by]] then
- controls.gamelist.insertbranch = game[group_by]
- branches[game[group_by]] = controls.gamelist.lastaddnode
- end
- controls.gamelist["addleaf"..branches[game[group_by]]] = game.name
- controls.gamelist["image"..controls.gamelist.lastaddnode] = getNodeImage(game)
- iup.TreeSetUserId(controls.gamelist, controls.gamelist.lastaddnode, game)
- end
-end
-
-function gui.editable(on)
- controls.editpane.active = on and "YES" or "NO"
-end
-
-
-function iup:GetType()
- local t = tostring(self):match("IUP%((.*)%)")
- if not t then
- return type(self)
- else
- return "iup_"..t
- end
-end
-
--- mapping between GUI widgets and game info struct fields
-local fieldmap = {
- -- information
- name = fields.title;
- comp = fields.compilation;
- _console_str = fields.system;
- _orig_console_str = fields.original_system;
- _region_str = fields.region;
- own = fields.ownership; -- 1..4 -> owned, formerly, borrowed, other
-
- -- progress
- complete = fields.status; -- 1..5 UBCMN
- achieve1 = fields.achievements;
- achieve2 = fields.max_achievements;
- online = fields.online_info;
- note = fields.notes;
-
- -- review
- _stars = fields.rating;
- comments = fields.comments;
-
- -- misc
- playing = fields.now_playing;
- wishlist = fields.wishlist;
-}
-
--- load all of the information for game into the editing fields
-function gui.loadFields(game)
- local set = {}
-
- function set:iup_text(value)
- self.value = value or ""
- end
-
- function set:iup_list(value)
- if not value then
- self.value = 0; return
- end
- for i=1,self.count do
- if self[i] == value then
- self.value = i
- return
- end
- end
- self.value = 0
- end
-
- function set:table(value)
- if self.writemap then
- value = self.writemap[value] or value
- end
-
- if not value then
- table.map(self, function(w) w.value = "OFF" end)
- elseif self[value] then
- self[value].value = "ON"
- else
- gui.warn("Display Game", "Couldn't set table:\n"..debug.traceback())
- end
- end
-
-
- for field,widget in pairs(fieldmap) do
- if set[iup.GetType(widget)] then
- set[iup.GetType(widget)](widget, game[field])
- else
- print("Warning: couldn't load/set field", field, game[field], iup.GetType(widget))
- end
- end
-
- fields.game = game
-
- gui.editable(game._dirty)
-end
-
-function gui.saveFields(game)
- local get = {}
-
- function get:iup_text()
- return self.value
- end
-
- function get:iup_list()
- if self.value == 0 then
- return ""
- else
- return self[self.value]
- end
- end
-
- function get:table()
- for i,widget in ipairs(self) do
- if widget.value == "ON" then
- if self.readmap then
- i = self.readmap[i] or i
- end
- return i
- end
- end
- return nil
- end
-
- for field,widget in pairs(fieldmap) do
- if get[iup.GetType(widget)] then
- game[field] = get[iup.GetType(widget)](widget)
- else
- print("Warning: couldn't read field", field, game[field], iup.GetType(widget))
- end
- end
-end
-
-return gui
View
175 bledit/window.lua
@@ -1,175 +0,0 @@
-local fields = require "bledit.fields"
-local controls = require "bledit.controls"
-
--- create a mockup of an element. Used to create placeholders that will
--- later be replaced with the real thing
-local function mock(name, expand)
- if expand == true then
- expand = "YES"
- elseif expand == false then
- expand = nil
- end
- return iup.frame { iup.label { fgcolor = "255 0 0"; expand = expand; title = '['..name..']' } }
-end
-
--- create a frame with a title, as in frame "foo" { ... }
-local function frame(title)
- return function(init)
- init.title = title
- return iup.frame(init)
- end
-end
-
--- the main window
-local win = iup.dialog {
- title = "Backloggery Editor";
- fontstyle = "bold";
- active = "NO";
- --size = "QUARTERxQUARTER";
-
- iup.hbox {
- iup.vbox { -- contains game list, sort buttons
- fontstyle = "";
- expandchildren = "YES";
-
- controls.gamelist;
- mock("Sort By");
- mock("Group By");
- };
- iup.vbox { -- contains edit controls
- iup.vbox {
- name = "editpane";
- active = "NO";
- iup.frame { -- general game information
- title = "Game Information";
-
- iup.vbox {
- fontstyle = "";
-
- frame "Title" {
- fields.title;
- };
- frame "Compilation" {
- fields.compilation;
- };
- iup.hbox {
- frame "System" {
- fields.system;
- };
- frame "Original System" {
- fields.original_system;
- };
- frame "Region" {
- fields.region;
- };
- };
- frame "Ownership" {
- iup.radio{
- iup.hbox {
- alignment = "ACENTER";
-
- fields.ownership[1];
- iup.fill { size = 20 };
- fields.ownership[2];
- iup.fill { size = 20 };
- fields.ownership[3];
- iup.fill { size = 20 };
- fields.ownership[4];
- iup.fill {};
- };
- };
- };
- };
- };
-
- iup.frame { -- status and progress controls
- title = "Progress";
-
- iup.vbox {
- fontstyle = "";
-
- iup.frame {
- title = "Status";
- iup.radio{
- iup.hbox {
- alignment = "ACENTER";
-
- fields.status[1];
- iup.fill { size = 20 };
- fields.status[2];
- iup.fill { size = 20 };
- fields.status[3];
- iup.fill { size = 20 };
- fields.status[4];
- iup.fill { size = 20 };
- fields.status[5];
- iup.fill {};
- };
- };
- };
-
- iup.hbox {
- iup.frame {
- title = "Achievements";
- iup.hbox {
- fields.achievements;
- iup.label { title = " out of " };
- fields.max_achievements;
- };
- };
- iup.frame {
- title = "Online Info";
-
- fields.online_info;
- };
- };
-
- iup.frame {
- title = "Notes";
- fields.notes;
- };
- };
- };
-
- iup.frame { -- rating and review controls
- title = "Review";
- iup.hbox {
- fontstyle = "";
- iup.frame {
- title = "Rating";
- iup.radio {
- iup.vbox {
- fontstyle = "";
- expandchildren = "YES";
-
- unpack(fields.rating);
- };
- };
- };
- iup.frame {
- title = "Comments";
-
- fields.comments;
- };
- };
- };
- };
- iup.hbox { -- command buttons
- fontstyle = "";
- alignment = "ACENTER";
-
- iup.fill {};
- controls.name;
- iup.fill {};
- controls.new;
- controls.edit;
- controls.delete;
- iup.fill {};
- controls.save;
- iup.fill {};
- };
- };
- };
-}
-
-return win
View
6 categories.lua
@@ -2,6 +2,7 @@ function initlibs()
require "lfs"
require "util.io"
require "libsteam"
+ require "config"
-- like lfs.dir, but returns an iterator over all directory entries that
-- don't start with .
@@ -91,11 +92,12 @@ end
function main(...)
initlibs()
+ local cfg = config.load("steamtools.cfg")
-- initialize Steam
- local path = (...) or io.prompt("Steam location (drag-and drop steam.exe): ")
+ local path = (...) or cfg.STEAM or io.prompt("Steam location (drag-and drop steam.exe): ")
- local steam,err = steam.open(path:gsub('^"(.*)"$', '%1'))
+ local steam,err = steam.open(path:gsub('^"(.*)"$', '%1'), cfg.STEAMNAME, unpack(cfg.STEAMID or {}))
if not steam then
io.eprintf("Couldn't read Steam directory: %s\n", err)
return 1
View
43 config.lua
@@ -0,0 +1,43 @@
+config = {}
+
+function config.load(file)
+ local cfg = {
+ EDITOR = "notepad";
+ IGNORE = "";
+ }
+
+ if loadfile(file) then
+ local f = loadfile(file)
+ setfenv(f, cfg)
+ f()
+ else
+ io.eprintf("Warning: couldn't load %s\n Error message was: %s\n Using default configuration\n\n"
+ , tostring(file)
+ , tostring(select(2, loadfile(file))))
+ end
+
+ -- build the set of ignore-game patterns
+ local patterns = {}
+ for pattern in cfg.IGNORE:gmatch("%s*([^\n]+)") do
+ pattern = pattern:gsub("%W"
+ , function(c)
+ if c == "*" then
+ return ".*"
+ else
+ return "%"..c
+ end
+ end)
+ patterns[#patterns+1] = pattern
+ end
+
+ function cfg:ignored(name)
+ for _,pattern in ipairs(patterns) do
+ if name:match(pattern) then
+ return true
+ end
+ end
+ return false
+ end
+
+ return cfg
+end
View
BIN images/1_5stars.bmp
Binary file not shown.
View
BIN images/2_5stars.bmp
Binary file not shown.
View
BIN images/3_5stars.bmp
Binary file not shown.
View
BIN images/4_5stars.bmp
Binary file not shown.
View
BIN images/5_5stars.bmp
Binary file not shown.
View
BIN images/beaten.bmp
Binary file not shown.
View
BIN images/completed.bmp
Binary file not shown.
View
BIN images/delete.bmp
Binary file not shown.
View
BIN images/edit.bmp
Binary file not shown.
View
BIN images/mastered.bmp
Binary file not shown.
View
BIN images/nowplaying.bmp
Binary file not shown.
View
BIN images/null.bmp
Binary file not shown.
View
BIN images/own_borrow.bmp
Binary file not shown.
View
BIN images/own_ghost.bmp
Binary file not shown.
View
BIN images/own_other.bmp
Binary file not shown.
View
BIN images/own_owned.bmp
Binary file not shown.
View
BIN images/unfinished.bmp
Binary file not shown.
View
BIN images/wishlist.bmp
Binary file not shown.
View
292 lib/ltn12.lua
@@ -0,0 +1,292 @@
+-----------------------------------------------------------------------------
+-- LTN12 - Filters, sources, sinks and pumps.
+-- LuaSocket toolkit.
+-- Author: Diego Nehab
+-- RCS ID: $Id: ltn12.lua,v 1.31 2006/04/03 04:45:42 diego Exp $
+-----------------------------------------------------------------------------
+
+-----------------------------------------------------------------------------
+-- Declare module
+-----------------------------------------------------------------------------
+local string = require("string")
+local table = require("table")
+local base = _G
+module("ltn12")
+
+filter = {}
+source = {}
+sink = {}
+pump = {}
+
+-- 2048 seems to be better in windows...
+BLOCKSIZE = 2048
+_VERSION = "LTN12 1.0.1"
+
+-----------------------------------------------------------------------------
+-- Filter stuff
+-----------------------------------------------------------------------------
+-- returns a high level filter that cycles a low-level filter
+function filter.cycle(low, ctx, extra)
+ base.assert(low)
+ return function(chunk)
+ local ret
+ ret, ctx = low(ctx, chunk, extra)
+ return ret
+ end
+end
+
+-- chains a bunch of filters together
+-- (thanks to Wim Couwenberg)
+function filter.chain(...)
+ local n = table.getn(arg)
+ local top, index = 1, 1
+ local retry = ""
+ return function(chunk)
+ retry = chunk and retry
+ while true do
+ if index == top then
+ chunk = arg[index](chunk)
+ if chunk == "" or top == n then return chunk
+ elseif chunk then index = index + 1
+ else
+ top = top+1
+ index = top
+ end
+ else
+ chunk = arg[index](chunk or "")
+ if chunk == "" then
+ index = index - 1
+ chunk = retry
+ elseif chunk then
+ if index == n then return chunk
+ else index = index + 1 end
+ else base.error("filter returned inappropriate nil") end
+ end
+ end
+ end
+end
+
+-----------------------------------------------------------------------------
+-- Source stuff
+-----------------------------------------------------------------------------
+-- create an empty source
+local function empty()
+ return nil
+end
+
+function source.empty()
+ return empty
+end
+
+-- returns a source that just outputs an error
+function source.error(err)
+ return function()
+ return nil, err
+ end
+end
+
+-- creates a file source
+function source.file(handle, io_err)
+ if handle then
+ return function()
+ local chunk = handle:read(BLOCKSIZE)
+ if not chunk then handle:close() end
+ return chunk
+ end
+ else return source.error(io_err or "unable to open file") end
+end
+
+-- turns a fancy source into a simple source
+function source.simplify(src)
+ base.assert(src)
+ return function()
+ local chunk, err_or_new = src()
+ src = err_or_new or src
+ if not chunk then return nil, err_or_new
+ else return chunk end
+ end
+end
+
+-- creates string source
+function source.string(s)
+ if s then
+ local i = 1
+ return function()
+ local chunk = string.sub(s, i, i+BLOCKSIZE-1)
+ i = i + BLOCKSIZE
+ if chunk ~= "" then return chunk
+ else return nil end
+ end
+ else return source.empty() end
+end
+
+-- creates rewindable source
+function source.rewind(src)
+ base.assert(src)
+ local t = {}
+ return function(chunk)
+ if not chunk then
+ chunk = table.remove(t)
+ if not chunk then return src()
+ else return chunk end
+ else
+ table.insert(t, chunk)
+ end
+ end
+end
+
+function source.chain(src, f)
+ base.assert(src and f)
+ local last_in, last_out = "", ""
+ local state = "feeding"
+ local err
+ return function()
+ if not last_out then
+ base.error('source is empty!', 2)
+ end
+ while true do
+ if state == "feeding" then
+ last_in, err = src()
+ if err then return nil, err end
+ last_out = f(last_in)
+ if not last_out then
+ if last_in then
+ base.error('filter returned inappropriate nil')
+ else
+ return nil
+ end
+ elseif last_out ~= "" then
+ state = "eating"
+ if last_in then last_in = "" end
+ return last_out
+ end
+ else
+ last_out = f(last_in)
+ if last_out == "" then
+ if last_in == "" then
+ state = "feeding"
+ else
+ base.error('filter returned ""')
+ end
+ elseif not last_out then
+ if last_in then
+ base.error('filter returned inappropriate nil')
+ else
+ return nil
+ end
+ else
+ return last_out
+ end
+ end
+ end
+ end
+end
+
+-- creates a source that produces contents of several sources, one after the
+-- other, as if they were concatenated
+-- (thanks to Wim Couwenberg)
+function source.cat(...)
+ local src = table.remove(arg, 1)
+ return function()
+ while src do
+ local chunk, err = src()
+ if chunk then return chunk end
+ if err then return nil, err end
+ src = table.remove(arg, 1)
+ end
+ end
+end
+
+-----------------------------------------------------------------------------
+-- Sink stuff
+-----------------------------------------------------------------------------
+-- creates a sink that stores into a table
+function sink.table(t)
+ t = t or {}
+ local f = function(chunk, err)
+ if chunk then table.insert(t, chunk) end
+ return 1
+ end
+ return f, t
+end
+
+-- turns a fancy sink into a simple sink
+function sink.simplify(snk)
+ base.assert(snk)
+ return function(chunk, err)
+ local ret, err_or_new = snk(chunk, err)
+ if not ret then return nil, err_or_new end
+ snk = err_or_new or snk
+ return 1
+ end
+end
+
+-- creates a file sink
+function sink.file(handle, io_err)
+ if handle then
+ return function(chunk, err)
+ if not chunk then
+ handle:close()
+ return 1
+ else return handle:write(chunk) end
+ end
+ else return sink.error(io_err or "unable to open file") end
+end
+
+-- creates a sink that discards data
+local function null()
+ return 1
+end
+
+function sink.null()
+ return null
+end
+
+-- creates a sink that just returns an error
+function sink.error(err)
+ return function()
+ return nil, err
+ end
+end
+
+-- chains a sink with a filter
+function sink.chain(f, snk)
+ base.assert(f and snk)
+ return function(chunk, err)
+ if chunk ~= "" then
+ local filtered = f(chunk)
+ local done = chunk and ""
+ while true do
+ local ret, snkerr = snk(filtered, err)
+ if not ret then return nil, snkerr end
+ if filtered == done then return 1 end
+ filtered = f(done)
+ end
+ else return 1 end
+ end
+end
+
+-----------------------------------------------------------------------------
+-- Pump stuff
+-----------------------------------------------------------------------------
+-- pumps one chunk from the source to the sink
+function pump.step(src, snk)
+ local chunk, src_err = src()
+ local ret, snk_err = snk(chunk, src_err)
+ if chunk and ret then return 1
+ else return nil, src_err or snk_err end
+end
+
+-- pumps all data from a source to a sink, using a step function
+function pump.all(src, snk, step)
+ base.assert(src and snk)
+ step = step or pump.step
+ while true do
+ local ret, err = step(src, snk)
+ if not ret then
+ if err then return nil, err
+ else return 1 end
+ end
+ end
+end
+
View
87 lib/mime.lua
@@ -0,0 +1,87 @@
+-----------------------------------------------------------------------------
+-- MIME support for the Lua language.
+-- Author: Diego Nehab
+-- Conforming to RFCs 2045-2049
+-- RCS ID: $Id: mime.lua,v 1.29 2007/06/11 23:44:54 diego Exp $
+-----------------------------------------------------------------------------
+
+-----------------------------------------------------------------------------
+-- Declare module and import dependencies
+-----------------------------------------------------------------------------
+local base = _G
+local ltn12 = require("ltn12")
+local mime = require("mime.core")
+local io = require("io")
+local string = require("string")
+module("mime")
+
+-- encode, decode and wrap algorithm tables
+encodet = {}
+decodet = {}
+wrapt = {}
+
+-- creates a function that chooses a filter by name from a given table
+local function choose(table)
+ return function(name, opt1, opt2)
+ if base.type(name) ~= "string" then
+ name, opt1, opt2 = "default", name, opt1
+ end
+ local f = table[name or "nil"]
+ if not f then
+ base.error("unknown key (" .. base.tostring(name) .. ")", 3)
+ else return f(opt1, opt2) end
+ end
+end
+
+-- define the encoding filters
+encodet['base64'] = function()
+ return ltn12.filter.cycle(b64, "")
+end
+
+encodet['quoted-printable'] = function(mode)
+ return ltn12.filter.cycle(qp, "",
+ (mode == "binary") and "=0D=0A" or "\r\n")
+end
+
+-- define the decoding filters
+decodet['base64'] = function()
+ return ltn12.filter.cycle(unb64, "")
+end
+
+decodet['quoted-printable'] = function()
+ return ltn12.filter.cycle(unqp, "")
+end
+
+local function format(chunk)
+ if chunk then
+ if chunk == "" then return "''"
+ else return string.len(chunk) end
+ else return "nil" end
+end
+
+-- define the line-wrap filters
+wrapt['text'] = function(length)
+ length = length or 76
+ return ltn12.filter.cycle(wrp, length, length)
+end
+wrapt['base64'] = wrapt['text']
+wrapt['default'] = wrapt['text']
+
+wrapt['quoted-printable'] = function()
+ return ltn12.filter.cycle(qpwrp, 76, 76)
+end
+
+-- function that choose the encoding, decoding or wrap algorithm
+encode = choose(encodet)
+decode = choose(decodet)
+wrap = choose(wrapt)
+
+-- define the end-of-line normalization filter
+function normalize(marker)
+ return ltn12.filter.cycle(eol, 0, marker)
+end
+
+-- high level stuffing filter
+function stuff()
+ return ltn12.filter.cycle(dot, 2)
+end
View
133 lib/socket.lua
@@ -0,0 +1,133 @@
+-----------------------------------------------------------------------------
+-- LuaSocket helper module
+-- Author: Diego Nehab
+-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
+-----------------------------------------------------------------------------
+
+-----------------------------------------------------------------------------
+-- Declare module and import dependencies
+-----------------------------------------------------------------------------
+local base = _G
+local string = require("string")
+local math = require("math")
+local socket = require("socket.core")
+module("socket")
+
+-----------------------------------------------------------------------------
+-- Exported auxiliar functions
+-----------------------------------------------------------------------------
+function connect(address, port, laddress, lport)
+ local sock, err = socket.tcp()
+ if not sock then return nil, err end
+ if laddress then
+ local res, err = sock:bind(laddress, lport, -1)
+ if not res then return nil, err end
+ end
+ local res, err = sock:connect(address, port)
+ if not res then return nil, err end
+ return sock
+end
+
+function bind(host, port, backlog)
+ local sock, err = socket.tcp()
+ if not sock then return nil, err end
+ sock:setoption("reuseaddr", true)
+ local res, err = sock:bind(host, port)
+ if not res then return nil, err end
+ res, err = sock:listen(backlog)
+ if not res then return nil, err end
+ return sock
+end
+
+try = newtry()
+
+function choose(table)
+ return function(name, opt1, opt2)
+ if base.type(name) ~= "string" then
+ name, opt1, opt2 = "default", name, opt1
+ end
+ local f = table[name or "nil"]
+ if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
+ else return f(opt1, opt2) end
+ end
+end
+
+-----------------------------------------------------------------------------
+-- Socket sources and sinks, conforming to LTN12
+-----------------------------------------------------------------------------
+-- create namespaces inside LuaSocket namespace
+sourcet = {}
+sinkt = {}
+
+BLOCKSIZE = 2048
+
+sinkt["close-when-done"] = function(sock)
+ return base.setmetatable({
+ getfd = function() return sock:getfd() end,
+ dirty = function() return sock:dirty() end
+ }, {
+ __call = function(self, chunk, err)
+ if not chunk then
+ sock:close()
+ return 1
+ else return sock:send(chunk) end
+ end
+ })
+end
+
+sinkt["keep-open"] = function(sock)
+ return base.setmetatable({
+ getfd = function() return sock:getfd() end,
+ dirty = function() return sock:dirty() end
+ }, {
+ __call = function(self, chunk, err)
+ if chunk then return sock:send(chunk)
+ else return 1 end
+ end
+ })
+end
+
+sinkt["default"] = sinkt["keep-open"]
+
+sink = choose(sinkt)
+
+sourcet["by-length"] = function(sock, length)
+ return base.setmetatable({
+ getfd = function() return sock:getfd() end,
+ dirty = function() return sock:dirty() end
+ }, {
+ __call = function()
+ if length <= 0 then return nil end
+ local size = math.min(socket.BLOCKSIZE, length)
+ local chunk, err = sock:receive(size)
+ if err then return nil, err end
+ length = length - string.len(chunk)
+ return chunk
+ end
+ })
+end
+
+sourcet["until-closed"] = function(sock)
+ local done
+ return base.setmetatable({
+ getfd = function() return sock:getfd() end,
+ dirty = function() return sock:dirty() end
+ }, {
+ __call = function()
+ if done then return nil end
+ local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
+ if not err then return chunk
+ elseif err == "closed" then
+ sock:close()
+ done = 1
+ return partial
+ else return nil, err end
+ end
+ })
+end
+
+
+sourcet["default"] = sourcet["until-closed"]
+
+source = choose(sourcet)
+
View
281 lib/socket/ftp.lua
@@ -0,0 +1,281 @@
+-----------------------------------------------------------------------------
+-- FTP support for the Lua language
+-- LuaSocket toolkit.
+-- Author: Diego Nehab
+-- RCS ID: $Id: ftp.lua,v 1.45 2007/07/11 19:25:47 diego Exp $
+-----------------------------------------------------------------------------
+
+-----------------------------------------------------------------------------
+-- Declare module and import dependencies
+-----------------------------------------------------------------------------
+local base = _G
+local table = require("table")
+local string = require("string")
+local math = require("math")
+local socket = require("socket")
+local url = require("socket.url")
+local tp = require("socket.tp")
+local ltn12 = require("ltn12")
+module("socket.ftp")
+
+-----------------------------------------------------------------------------
+-- Program constants
+-----------------------------------------------------------------------------
+-- timeout in seconds before the program gives up on a connection
+TIMEOUT = 60
+-- default port for ftp service
+PORT = 21
+-- this is the default anonymous password. used when no password is
+-- provided in url. should be changed to your e-mail.
+USER = "ftp"
+PASSWORD = "anonymous@anonymous.org"
+
+-----------------------------------------------------------------------------
+-- Low level FTP API
+-----------------------------------------------------------------------------
+local metat = { __index = {} }
+
+function open(server, port, create)
+ local tp = socket.try(tp.connect(server, port or PORT, TIMEOUT, create))
+ local f = base.setmetatable({ tp = tp }, metat)
+ -- make sure everything gets closed in an exception
+ f.try = socket.newtry(function() f:close() end)
+ return f
+end
+
+function metat.__index:portconnect()
+ self.try(self.server:settimeout(TIMEOUT))
+ self.data = self.try(self.server:accept())
+ self.try(self.data:settimeout(TIMEOUT))
+end
+
+function metat.__index:pasvconnect()
+ self.data = self.try(socket.tcp())
+ self.try(self.data:settimeout(TIMEOUT))
+ self.try(self.data:connect(self.pasvt.ip, self.pasvt.port))
+end
+
+function metat.__index:login(user, password)
+ self.try(self.tp:command("user", user or USER))
+ local code, reply = self.try(self.tp:check{"2..", 331})
+ if code == 331 then
+ self.try(self.tp:command("pass", password or PASSWORD))
+ self.try(self.tp:check("2.."))
+ end
+ return 1
+end
+
+function metat.__index:pasv()
+ self.try(self.tp:command("pasv"))
+ local code, reply = self.try(self.tp:check("2.."))
+ local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)"
+ local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern))
+ self.try(a and b and c and d and p1 and p2, reply)
+ self.pasvt = {
+ ip = string.format("%d.%d.%d.%d", a, b, c, d),
+ port = p1*256 + p2
+ }
+ if self.server then
+ self.server:close()
+ self.server = nil
+ end
+ return self.pasvt.ip, self.pasvt.port
+end
+
+function metat.__index:port(ip, port)
+ self.pasvt = nil
+ if not ip then
+ ip, port = self.try(self.tp:getcontrol():getsockname())
+ self.server = self.try(socket.bind(ip, 0))
+ ip, port = self.try(self.server:getsockname())
+ self.try(self.server:settimeout(TIMEOUT))
+ end
+ local pl = math.mod(port, 256)
+ local ph = (port - pl)/256
+ local arg = string.gsub(string.format("%s,%d,%d", ip, ph, pl), "%.", ",")
+ self.try(self.tp:command("port", arg))
+ self.try(self.tp:check("2.."))
+ return 1
+end
+
+function metat.__index:send(sendt)
+ self.try(self.pasvt or self.server, "need port or pasv first")
+ -- if there is a pasvt table, we already sent a PASV command
+ -- we just get the data connection into self.data
+ if self.pasvt then self:pasvconnect() end
+ -- get the transfer argument and command
+ local argument = sendt.argument or
+ url.unescape(string.gsub(sendt.path or "", "^[/\\]", ""))
+ if argument == "" then argument = nil end
+ local command = sendt.command or "stor"
+ -- send the transfer command and check the reply
+ self.try(self.tp:command(command, argument))
+ local code, reply = self.try(self.tp:check{"2..", "1.."})
+ -- if there is not a a pasvt table, then there is a server
+ -- and we already sent a PORT command
+ if not self.pasvt then self:portconnect() end
+ -- get the sink, source and step for the transfer
+ local step = sendt.step or ltn12.pump.step
+ local readt = {self.tp.c}
+ local checkstep = function(src, snk)
+ -- check status in control connection while downloading
+ local readyt = socket.select(readt, nil, 0)
+ if readyt[tp] then code = self.try(self.tp:check("2..")) end
+ return step(src, snk)
+ end
+ local sink = socket.sink("close-when-done", self.data)
+ -- transfer all data and check error
+ self.try(ltn12.pump.all(sendt.source, sink, checkstep))
+ if string.find(code, "1..") then self.try(self.tp:check("2..")) end
+ -- done with data connection
+ self.data:close()
+ -- find out how many bytes were sent
+ local sent = socket.skip(1, self.data:getstats())
+ self.data = nil
+ return sent
+end
+
+function metat.__index:receive(recvt)
+ self.try(self.pasvt or self.server, "need port or pasv first")
+ if self.pasvt then self:pasvconnect() end
+ local argument = recvt.argument or
+ url.unescape(string.gsub(recvt.path or "", "^[/\\]", ""))
+ if argument == "" then argument = nil end
+ local command = recvt.command or "retr"
+ self.try(self.tp:command(command, argument))
+ local code = self.try(self.tp:check{"1..", "2.."})
+ if not self.pasvt then self:portconnect() end
+ local source = socket.source("until-closed", self.data)
+ local step = recvt.step or ltn12.pump.step
+ self.try(ltn12.pump.all(source, recvt.sink, step))
+ if string.find(code, "1..") then self.try(self.tp:check("2..")) end
+ self.data:close()
+ self.data = nil
+ return 1
+end
+
+function metat.__index:cwd(dir)
+ self.try(self.tp:command("cwd", dir))
+ self.try(self.tp:check(250))
+ return 1
+end
+
+function metat.__index:type(type)
+ self.try(self.tp:command("type", type))
+ self.try(self.tp:check(200))
+ return 1
+end
+
+function metat.__index:greet()
+ local code = self.try(self.tp:check{"1..", "2.."})
+ if string.find(code, "1..") then self.try(self.tp:check("2..")) end
+ return 1
+end
+
+function metat.__index:quit()
+ self.try(self.tp:command("quit"))
+ self.try(self.tp:check("2.."))
+ return 1
+end
+
+function metat.__index:close()
+ if self.data then self.data:close() end
+ if self.server then self.server:close() end
+ return self.tp:close()
+end
+
+-----------------------------------------------------------------------------
+-- High level FTP API
+-----------------------------------------------------------------------------
+local function override(t)
+ if t.url then
+ local u = url.parse(t.url)
+ for i,v in base.pairs(t) do
+ u[i] = v
+ end
+ return u
+ else return t end
+end
+
+local function tput(putt)
+ putt = override(putt)
+ socket.try(putt.host, "missing hostname")
+ local f = open(putt.host, putt.port, putt.create)
+ f:greet()
+ f:login(putt.user, putt.password)
+ if putt.type then f:type(putt.type) end
+ f:pasv()
+ local sent = f:send(putt)
+ f:quit()
+ f:close()
+ return sent
+end
+
+local default = {
+ path = "/",
+ scheme = "ftp"
+}
+
+local function parse(u)
+ local t = socket.try(url.parse(u, default))
+ socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'")
+ socket.try(t.host, "missing hostname")
+ local pat = "^type=(.)$"
+ if t.params then
+ t.type = socket.skip(2, string.find(t.params, pat))
+ socket.try(t.type == "a" or t.type == "i",
+ "invalid type '" .. t.type .. "'")
+ end
+ return t
+end
+
+local function sput(u, body)
+ local putt = parse(u)
+ putt.source = ltn12.source.string(body)
+ return tput(putt)
+end
+
+put = socket.protect(function(putt, body)
+ if base.type(putt) == "string" then return sput(putt, body)
+ else return tput(putt) end
+end)
+
+local function tget(gett)
+ gett = override(gett)
+ socket.try(gett.host, "missing hostname")
+ local f = open(gett.host, gett.port, gett.create)
+ f:greet()
+ f:login(gett.user, gett.password)
+ if gett.type then f:type(gett.type) end
+ f:pasv()
+ f:receive(gett)
+ f:quit()
+ return f:close()
+end
+
+local function sget(u)
+ local gett = parse(u)
+ local t = {}
+ gett.sink = ltn12.sink.table(t)
+ tget(gett)
+ return table.concat(t)
+end
+
+command = socket.protect(function(cmdt)
+ cmdt = override(cmdt)
+ socket.try(cmdt.host, "missing hostname")
+ socket.try(cmdt.command, "missing command")
+ local f = open(cmdt.host, cmdt.port, cmdt.create)
+ f:greet()
+ f:login(cmdt.user, cmdt.password)
+ f.try(f.tp:command(cmdt.command, cmdt.argument))
+ if cmdt.check then f.try(f.tp:check(cmdt.check)) end
+ f:quit()
+ return f:close()
+end)
+
+get = socket.protect(function(gett)
+ if base.type(gett) == "string" then return sget(gett)
+ else return tget(gett) end
+end)
+
View
350 lib/socket/http.lua
@@ -0,0 +1,350 @@
+-----------------------------------------------------------------------------
+-- HTTP/1.1 client support for the Lua language.
+-- LuaSocket toolkit.
+-- Author: Diego Nehab
+-- RCS ID: $Id: http.lua,v 1.70 2007/03/12 04:08:40 diego Exp $
+-----------------------------------------------------------------------------
+
+-----------------------------------------------------------------------------
+-- Declare module and import dependencies
+-------------------------------------------------------------------------------
+local socket = require("socket")
+local url = require("socket.url")
+local ltn12 = require("ltn12")
+local mime = require("mime")
+local string = require("string")
+local base = _G
+local table = require("table")
+module("socket.http")
+
+-----------------------------------------------------------------------------
+-- Program constants
+-----------------------------------------------------------------------------
+-- connection timeout in seconds
+TIMEOUT = 60
+-- default port for document retrieval
+PORT = 80
+-- user agent field sent in request
+USERAGENT = socket._VERSION
+
+-----------------------------------------------------------------------------
+-- Reads MIME headers from a connection, unfolding where needed
+-----------------------------------------------------------------------------
+local function receiveheaders(sock, headers)
+ local line, name, value, err
+ headers = headers or {}
+ -- get first line
+ line, err = sock:receive()
+ if err then return nil, err end
+ -- headers go until a blank line is found
+ while line ~= "" do
+ -- get field-name and value
+ name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)"))
+ if not (name and value) then return nil, "malformed reponse headers" end
+ name = string.lower(name)
+ -- get next line (value might be folded)
+ line, err = sock:receive()
+ if err then return nil, err end
+ -- unfold any folded values
+ while string.find(line, "^%s") do
+ value = value .. line
+ line = sock:receive()
+ if err then return nil, err end
+ end
+ -- save pair in table
+ if headers[name] then headers[name] = headers[name] .. ", " .. value
+ else headers[name] = value end
+ end
+ return headers
+end
+
+-----------------------------------------------------------------------------
+-- Extra sources and sinks
+-----------------------------------------------------------------------------
+socket.sourcet["http-chunked"] = function(sock, headers)
+ return base.setmetatable({
+ getfd = function() return sock:getfd() end,
+ dirty = function() return sock:dirty() end
+ }, {
+ __call = function()
+ -- get chunk size, skip extention
+ local line, err = sock:receive()
+ if err then return nil, err end
+ local size = base.tonumber(string.gsub(line, ";.*", ""), 16)
+ if not size then return nil, "invalid chunk size" end
+ -- was it the last chunk?
+ if size > 0 then
+ -- if not, get chunk and skip terminating CRLF
+ local chunk, err, part = sock:receive(size)
+ if chunk then sock:receive() end
+ return chunk, err
+ else
+ -- if it was, read trailers into headers table
+ headers, err = receiveheaders(sock, headers)
+ if not headers then return nil, err end
+ end
+ end
+ })
+end
+
+socket.sinkt["http-chunked"] = function(sock)
+ return base.setmetatable({
+ getfd = function() return sock:getfd() end,
+ dirty = function() return sock:dirty() end
+ }, {
+ __call = function(self, chunk, err)
+ if not chunk then return sock:send("0\r\n\r\n") end
+ local size = string.format("%X\r\n", string.len(chunk))
+ return sock:send(size .. chunk .. "\r\n")
+ end
+ })
+end
+
+-----------------------------------------------------------------------------
+-- Low level HTTP API
+-----------------------------------------------------------------------------
+local metat = { __index = {} }
+
+function open(host, port, create)
+ -- create socket with user connect function, or with default
+ local c = socket.try((create or socket.tcp)())
+ local h = base.setmetatable({ c = c }, metat)
+ -- create finalized try
+ h.try = socket.newtry(function() h:close() end)
+ -- set timeout before connecting
+ h.try(c:settimeout(TIMEOUT))
+ h.try(c:connect(host, port or PORT))
+ -- here everything worked
+ return h
+end
+
+function metat.__index:sendrequestline(method, uri)
+ local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri)
+ return self.try(self.c:send(reqline))
+end
+
+function metat.__index:sendheaders(headers)
+ local h = "\r\n"
+ for i, v in base.pairs(headers) do
+ h = i .. ": " .. v .. "\r\n" .. h
+ end
+ self.try(self.c:send(h))
+ return 1
+end
+
+function metat.__index:sendbody(headers, source, step)
+ source = source or ltn12.source.empty()
+ step = step or ltn12.pump.step
+ -- if we don't know the size in advance, send chunked and hope for the best
+ local mode = "http-chunked"
+ if headers["content-length"] then mode = "keep-open" end
+ return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step))
+end
+
+function metat.__index:receivestatusline()
+ local status = self.try(self.c:receive(5))
+ -- identify HTTP/0.9 responses, which do not contain a status line
+ -- this is just a heuristic, but is what the RFC recommends
+ if status ~= "HTTP/" then return nil, status end
+ -- otherwise proceed reading a status line
+ status = self.try(self.c:receive("*l", status))
+ local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)"))
+ return self.try(base.tonumber(code), status)
+end
+
+function metat.__index:receiveheaders()
+ return self.try(receiveheaders(self.c))
+end
+
+function metat.__index:receivebody(headers, sink, step)
+ sink = sink or ltn12.sink.null()
+ step = step or ltn12.pump.step
+ local length = base.tonumber(headers["content-length"])
+ local t = headers["transfer-encoding"] -- shortcut
+ local mode = "default" -- connection close
+ if t and t ~= "identity" then mode = "http-chunked"
+ elseif base.tonumber(headers["content-length"]) then mode = "by-length" end
+ return self.try(ltn12.pump.all(socket.source(mode, self.c, length),
+ sink, step))
+end
+
+function metat.__index:receive09body(status, sink, step)
+ local source = ltn12.source.rewind(socket.source("until-closed", self.c))
+ source(status)
+ return self.try(ltn12.pump.all(source, sink, step))
+end
+
+function metat.__index:close()
+ return self.c:close()
+end
+
+-----------------------------------------------------------------------------
+-- High level HTTP API
+-----------------------------------------------------------------------------
+local function adjusturi(reqt)
+ local u = reqt
+ -- if there is a proxy, we need the full url. otherwise, just a part.
+ if not reqt.proxy and not PROXY then
+ u = {
+ path = socket.try(reqt.path, "invalid path 'nil'"),
+ params = reqt.params,
+ query = reqt.query,
+ fragment = reqt.fragment
+ }
+ end
+ return url.build(u)
+end
+
+local function adjustproxy(reqt)
+ local proxy = reqt.proxy or PROXY
+ if proxy then
+ proxy = url.parse(proxy)
+ return proxy.host, proxy.port or 3128
+ else
+ return reqt.host, reqt.port
+ end
+end
+
+local function adjustheaders(reqt)
+ -- default headers
+ local lower = {
+ ["user-agent"] = USERAGENT,
+ ["host"] = reqt.host,
+ ["connection"] = "close, TE",
+ ["te"] = "trailers"
+ }
+ -- if we have authentication information, pass it along
+ if reqt.user and reqt.password then
+ lower["authorization"] =
+ "Basic " .. (mime.b64(reqt.user .. ":" .. reqt.password))
+ end
+ -- override with user headers
+ for i,v in base.pairs(reqt.headers or lower) do
+ lower[string.lower(i)] = v
+ end
+ return lower
+end
+
+-- default url parts
+local default = {
+ host = "",
+ port = PORT,
+ path ="/",
+ scheme = "http"
+}
+
+local function adjustrequest(reqt)
+ -- parse url if provided
+ local nreqt = reqt.url and url.parse(reqt.url, default) or {}
+ -- explicit components override url
+ for i,v in base.pairs(reqt) do nreqt[i] = v end
+ if nreqt.port == "" then nreqt.port = 80 end
+ socket.try(nreqt.host and nreqt.host ~= "",
+ "invalid host '" .. base.tostring(nreqt.host) .. "'")
+ -- compute uri if user hasn't overriden
+ nreqt.uri = reqt.uri or adjusturi(nreqt)
+ -- ajust host and port if there is a proxy
+ nreqt.host, nreqt.port = adjustproxy(nreqt)
+ -- adjust headers in request
+ nreqt.headers = adjustheaders(nreqt)
+ return nreqt
+end
+
+local function shouldredirect(reqt, code, headers)
+ return headers.location and
+ string.gsub(headers.location, "%s", "") ~= "" and
+ (reqt.redirect ~= false) and
+ (code == 301 or code == 302) and
+ (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD")
+ and (not reqt.nredirects or reqt.nredirects < 5)
+end
+
+local function shouldreceivebody(reqt, code)
+ if reqt.method == "HEAD" then return nil end
+ if code == 204 or code == 304 then return nil end
+ if code >= 100 and code < 200 then return nil end
+ return 1
+end
+
+-- forward declarations
+local trequest, tredirect
+
+function tredirect(reqt, location)
+ local result, code, headers, status = trequest {
+ -- the RFC says the redirect URL has to be absolute, but some
+ -- servers do not respect that
+ url = url.absolute(reqt.url, location),
+ source = reqt.source,
+ sink = reqt.sink,
+ headers = reqt.headers,
+ proxy = reqt.proxy,
+ nredirects = (reqt.nredirects or 0) + 1,
+ create = reqt.create
+ }
+ -- pass location header back as a hint we redirected
+ headers = headers or {}
+ headers.location = headers.location or location
+ return result, code, headers, status
+end
+
+function trequest(reqt)
+ -- we loop until we get what we want, or
+ -- until we are sure there is no way to get it
+ local nreqt = adjustrequest(reqt)
+ local h = open(nreqt.host, nreqt.port, nreqt.create)
+ -- send request line and headers
+ h:sendrequestline(nreqt.method, nreqt.uri)
+ h:sendheaders(nreqt.headers)
+ -- if there is a body, send it
+ if nreqt.source then
+ h:sendbody(nreqt.headers, nreqt.source, nreqt.step)
+ end
+ local code, status = h:receivestatusline()
+ -- if it is an HTTP/0.9 server, simply get the body and we are done
+ if not code then
+ h:receive09body(status, nreqt.sink, nreqt.step)
+ return 1, 200
+ end
+ local headers
+ -- ignore any 100-continue messages
+ while code == 100 do
+ headers = h:receiveheaders()
+ code, status = h:receivestatusline()
+ end
+ headers = h:receiveheaders()
+ -- at this point we should have a honest reply from the server
+ -- we can't redirect if we already used the source, so we report the error
+ if shouldredirect(nreqt, code, headers) and not nreqt.source then
+ h:close()
+ return tredirect(reqt, headers.location)
+ end
+ -- here we are finally done
+ if shouldreceivebody(nreqt, code) then
+ h:receivebody(headers, nreqt.sink, nreqt.step)
+ end
+ h:close()
+ return 1, code, headers, status
+end
+
+local function srequest(u, b)
+ local t = {}
+ local reqt = {
+ url = u,
+ sink = ltn12.sink.table(t)
+ }
+ if b then
+ reqt.source = ltn12.source.string(b)
+ reqt.headers = {
+ ["content-length"] = string.len(b),
+ ["content-type"] = "application/x-www-form-urlencoded"
+ }
+ reqt.method = "POST"
+ end
+ local code, headers, status = socket.skip(1, trequest(reqt))
+ return table.concat(t), code, headers, status
+end
+
+request = socket.protect(function(reqt, body)
+ if base.type(reqt) == "string" then return srequest(reqt, body)
+ else return trequest(reqt) end
+end)
View
251 lib/socket/smtp.lua
@@ -0,0 +1,251 @@
+-----------------------------------------------------------------------------
+-- SMTP client support for the Lua language.
+-- LuaSocket toolkit.
+-- Author: Diego Nehab
+-- RCS ID: $Id: smtp.lua,v 1.46 2007/03/12 04:08:40 diego Exp $
+-----------------------------------------------------------------------------
+
+-----------------------------------------------------------------------------
+-- Declare module and import dependencies
+-----------------------------------------------------------------------------
+local base = _G
+local coroutine = require("coroutine")
+local string = require("string")
+local math = require("math")
+local os = require("os")
+local socket = require("socket")
+local tp = require("socket.tp")
+local ltn12 = require("ltn12")
+local mime = require("mime")
+module("socket.smtp")
+
+-----------------------------------------------------------------------------
+-- Program constants
+-----------------------------------------------------------------------------
+-- timeout for connection
+TIMEOUT = 60
+-- default server used to send e-mails
+SERVER = "localhost"
+-- default port
+PORT = 25
+-- domain used in HELO command and default sendmail
+-- If we are under a CGI, try to get from environment
+DOMAIN = os.getenv("SERVER_NAME") or "localhost"
+-- default time zone (means we don't know)
+ZONE = "-0000"
+
+---------------------------------------------------------------------------
+-- Low level SMTP API
+-----------------------------------------------------------------------------
+local metat = { __index = {} }
+
+function metat.__index:greet(domain)
+ self.try(self.tp:check("2.."))
+ self.try(self.tp:command("EHLO", domain or DOMAIN))
+ return socket.skip(1, self.try(self.tp:check("2..")))
+end
+
+function metat.__index:mail(from)
+ self.try(self.tp:command("MAIL", "FROM:" .. from))
+ return self.try(self.tp:check("2.."))
+end
+
+function metat.__index:rcpt(to)
+ self.try(self.tp:command("RCPT", "TO:" .. to))
+ return self.try(self.tp:check("2.."))
+end
+
+function metat.__index:data(src, step)
+ self.try(self.tp:command("DATA"))
+ self.try(self.tp:check("3.."))
+ self.try(self.tp:source(src, step))
+ self.try(self.tp:send("\r\n.\r\n"))
+ return self.try(self.tp:check("2.."))
+end
+
+function metat.__index:quit()
+ self.try(self.tp:command("QUIT"))
+ return self.try(self.tp:check("2.."))
+end
+
+function metat.__index:close()
+ return self.tp:close()
+end
+
+function metat.__index:login(user, password)
+ self.try(self.tp:command("AUTH", "LOGIN"))
+ self.try(self.tp:check("3.."))
+ self.try(self.tp:command(mime.b64(user)))
+ self.try(self.tp:check("3.."))
+ self.try(self.tp:command(mime.b64(password)))
+ return self.try(self.tp:check("2.."))
+end
+
+function metat.__index:plain(user, password)
+ local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password)
+ self.try(self.tp:command("AUTH", auth))
+ return self.try(self.tp:check("2.."))
+end
+
+function metat.__index:auth(user, password, ext)
+ if not user or not password then return 1 end
+ if string.find(ext, "AUTH[^\n]+LOGIN") then
+ return self:login(user, password)
+ elseif string.find(ext, "AUTH[^\n]+PLAIN") then
+ return self:plain(user, password)
+ else
+ self.try(nil, "authentication not supported")
+ end
+end
+
+-- send message or throw an exception
+function metat.__index:send(mailt)
+ self:mail(mailt.from)
+ if base.type(mailt.rcpt) == "table" then