Tuesday, March 15, 2016

my toothbrush's home

My toothbrush lives in a tube in my everyday bag, and like most of my stuff is also rainbow.

This is actually the second version of my toothbrush tube. The previous version was wrapped in painted hockey tape. It got really dirty after a while, so it had to go.

Now the tube is covered in acrylic paint and Mod Podge, which is basically white glue, leaving a plastic finish.

Having something looking like this got the attention from greyhound security when they searched my carry on bag.  This is a thing Greyhound does now, they check carry on bags for weapons and stuff, and make everyone empty their pockets, at the occasional terminal.  I got to see quite a look of surprise on the security person's face when they opened the tube and found a toothbrush.

Wednesday, February 3, 2016

"The nest has fridge poetry"

"The nest has fridge poetry" has been on the wall for several months.

The last person I saw with some on her fridge said that she had made it themselves, I think that was the story.

Today I went to Staples up the hill and picked up a package of magnetic paper, there were four sheets in the pack.  When I got back to the nest a quick Google search found do it yourself magnetic fridge poetry directions.

The parts:
- words
- word processor
- magnetic paper
- injet printer
- sissors

I took the words from the general service list, as the directions suggested.  Put the text of the word list in a text editor, cut the extra bits out of the text leaving only a list of words one per line.  Took that list and joined the lines together separated by three spaces resulting in a long spaced out line. Put that long line in OpenOffice writer, set the font to "Verdana" 12pt and collapsed the margins. Printed the first page. And cut up the magnet with scissors.

I still have two and a bit sheets of magnetic paper, I think I will do an analysis of one of my programming projects and find the most common words and symbols and make a sheet from that.  Programmer fridge poetry, is that a thing yet?  I can find Geek Fridge Poetry, but none for Java. Interesting.

Monday, February 1, 2016

Flour on the counter

I had an empty large container that formerly contained ground coffee, and wanted to put flour on the counter for easier access when making bread.  After cleaning the container out and drying it well it worked well. Later I had an empty flour bag and decided to make a label from it.  Even later I was at a Dollarama with Cassandra and she pointed out some rainbow wrapping paper and mentioned that I can cover things with it. With much gratitude I covered the silver container with Mod Podge and wrapping paper, another layer of Mod Podge and the label, and another over top of the label to seal it all down.

Wednesday, November 11, 2015

The nest has draft guards

The nest where I live is drafty, and the weather is getting cold again.  Last year I tried foam tape around the doors with the effect of making the door hard to close and still drafty.  The nest didn't get above 15c in the depth of winter, so I set the kitchen temperature to 10c and left the rest off. The power bill was $150/month.

Last spring I went on a mission to get a bunch of my stuff from storage in Halifax, including a big bag of worn out clothes and fabric scraps.

Last week I cut up the toes off some old socks and sewed them end to end making longer socks.  I took these socks and stuffed them with more worn out clothes. Finally I took these stuffed socks and tacked them to the drafty doors of the nest.

The outside door looses a lot of heat on the opening edge, and later I may do the same to the bottom of that door too.  It turns out that the metal front door is wood on the narrow edges.

Also, the bottom room gets below zero in the winter too, and has a big gap at the bottom of the door.

Both doors work as they usually do.

Over the next few months I get to see how effective this is.

Wednesday, October 28, 2015

Happy's memory card holder

Long ago I made a memory card holder that held three cards.  It traveled with me across the country at least twice, maybe all three times, and worked well to keep my memory cards from getting lost. Recently, after seeing the card holder I made for Ep1c I wanted one that looked like that for myself. So, I took the one I already had, took it apart, cut some more parts, painted it like a rainbow sandwich, and sewed it up into a memory card holder that holds six cards.

This is something I can be commissioned to make, say fifty dollar painted, or twenty unpainted?



Friday, January 10, 2014

Gmail Inbox Notification Widget

I wanted to display my Gmail Inbox unread message count on my personal web page on Google Sites, so I made a tiny Google Apps Script widget to do this.



The code turned out very simple.

code.gs:
function doGet() {
  return HtmlService.createTemplateFromFile('page')
    .evaluate()
    .setSandboxMode(HtmlService.SandboxMode.NATIVE);
}

function getInboxUnreadCount() {
  return GmailApp.getInboxUnreadCount();
}

page.html:
<style type="text/css">
#refresh { color: blue; text-decoration: underline; }
#refresh:hover { cursor: pointer; }
</style>

<div id="inbox">
Inbox (<span id="count">...</span>)
<span id="refresh">refresh</span>
</div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
function check() {
  clearTimeout(check.timer);
  google.script.run
    .withSuccessHandler(function(result) {
      $('#count').text(result);
      $('#inbox').toggleClass('unread', result > 0);
    })
    .getInboxUnreadCount();
  $('#count').text("...");
  check.timer = setTimeout(check, 600000);
}

$(function() {
  check();
  
  $('#refresh').click(check);
});

</script>

The getInboxUnreadCount() function needed to be run from the script editor to grant the needed permissions to the script, after that it worked from my personal home page.

Interactive Javascript Development

When I was traveling to a friend's place to give her a Christmas not alone I got picked up by someone who suggested I learn jQuery... so I did, and in the process wrote a JavaScript Read Eval Print Loop that makes jQuery and JavaScript development as easy as interactive Python or BASH programming.

The result is a single file web application, JavaScript Read, Eval, Print, Loop. View the source to see what was needed to make it work.

Monday, January 6, 2014

Facebook notification widget

I wanted to display my facebook notification information on my personal home screen.

I ended up with a single HTML file which I have hosted on Google Drive and embedded in an iframe in my personal home screen on Google Sites.

The end result is a single line where the numbers change if there are any notifications to report.



<html><head><title>Facebook Quick Check</title></head><body>
<style type="text/css">
body { margin: 0px; }
</style>

<div id="fb-root"></div>

<div>
    <span id="login"><a href="javascript:doLogin()">FB Login</a>: </span>
    Notice (<span id="notice">...</span>)
    Friend (<span id="friend">...</span>)
    Inbox (<span id="inbox">...</span>)
    <a href="javascript:loadStatus()">refresh</a>
</div>

<div id="out"></div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
I wanted a fluent API that felt like Google Apps Script server calls instead of the regular Facebook API, so I made a quick one...
function FBRun(parent) {
  this.successHandler = parent ? parent.successHandler : function(result) { };
  this.errorHandler = parent ? parent.errorHandler : function(result) { };
};
FBRun.prototype.withSuccessHandler = function(handler) {
  var obj = new FBRun(this);
  obj.successHandler = handler;
  return obj;
};
FBRun.prototype.withErrorHandler = function(handler) {
  var obj = new FBRun(this);
  obj.errorHandler = handler;
  return obj;
};
FBRun.prototype.getHandler = function() {
  var obj = this;
  return function(response) {
    if(!response || response.error) {
      return obj.errorHandler(response);
    }
    return obj.successHandler(response);
  }
};
FBRun.prototype.get = function(url, data) {
  FB.api(url, 'get', data, this.getHandler());
  return this;
};
FBRun.prototype.login = function(data) {
  FB.login(this.getHandler(), data);
  return this;
}
FBRun.prototype.loginStatus = function() {
  FB.getLoginStatus(this.getHandler());
  return this;
}
fbrun = new FBRun();
Load the Facebook API and check the login status on success...
$(document).ready(function() {
  $.ajaxSetup({ cache: true });
  $.getScript('//connect.facebook.net/en_UK/all.js', function(){
    FB.init({
      appId: 'APPLICATION_KEY',
    });
    fbrun
      .withSuccessHandler(onLogin)
      .loginStatus();
  });
});
The code for the login link...
function doLogin() {
  fbrun
    .withErrorHandler(display)
    .withSuccessHandler(onLogin)
    .login({scope: 'manage_notifications,read_requests,read_mailbox'});
}

var perms = {};

function onLogin(response) {
  if(response.status === 'connected') {
    fbrun
      .withErrorHandler(display)
      .withSuccessHandler(function(response) {
        perms = response.data[0];
        
        if(perms.manage_notifications && perms.read_requests && perms.read_mailbox) {
          $('#login').hide();
        }
        start();
      })
      .get('/me/permissions');
  }
}

function display(response) {
  $('#out').text(JSON.stringify(response));
}
Load the information and start the refresh timer...
function start() {
  loadStatus();
  
  if(start.active) {
    return;
  }
  start.active = true;
  
  function check() {
    loadStatus();
    setTimeout(check, 600000);
  }
  setTimeout(check, 600000);
}

function loadStatus() {
  if(perms.manage_notifications) {
    $('#notice').text('...');
    fbrun
      .withErrorHandler(display)
      .withSuccessHandler(function(response) {
        if(response.summary.unseen_count != undefined) {
          $('#notice').text(response.summary.unseen_count);
        } else {
          $('#notice').text("0");
        }
      })
      .get('/me/notifications', { limit: 0 });
  } else {
    $('#notice').text('-');
  }
  if(perms.read_requests) {
    $('#friend').text('...');
    fbrun
      .withErrorHandler(display)
      .withSuccessHandler(function(response) {
        $('#friend').text(response.summary.unread_count);
      })
      .get('/me/friendrequests', { limit: 0 });
  } else {
    $('#friend').text('-');
  }
  if(perms.read_mailbox) {
    $('#inbox').text('...');
    fbrun
      .withErrorHandler(display)
      .withSuccessHandler(function(response) {
        $('#inbox').text(response.summary.unseen_count);
      })
      .get('/me/inbox', { limit: 0 });
  } else {
    $('#inbox').text('-');
  }
}
</script>
</body></html>

Update: limit the data returned to just the summaries by setting the item limit to zero.

Saturday, January 4, 2014

Google Apps Script execution model

I was wondering if Google Apps Script web applications retained state between invocations, so I did a small experiment.

Using a Script created in Google Drive:

Code.gs:
function doGet() {
  return HtmlService.createTemplateFromFile('page')
    .evaluate()
    .setSandboxMode(HtmlService.SandboxMode.NATIVE);
}

var date = new Date();
var last = -1;

function func(value) {
  Utilities.sleep(100);
  var out = "date = " + date + ", last = " + last;
  last = value;
  return out;
}

page.html:
<style type="text/css">
pre {
  border: 1px solid green;
}
</style>

<div id="out">
<pre>loading...</pre>
</div>

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
$(function() {
  for(var i = 0; i < 2; i++) {
    launch(i);
  }
});

setTimeout(function() {
  for(var i = 2; i < 4; i++) {
    launch(i);
  }
}, 2000);

function launch(id) {
  $("<pre />").text("started " + id).appendTo('#out');
  google.script.run
    .withSuccessHandler(function(result) {
      $("<pre />").text("finished " + id + ": " + result).appendTo('#out');
    })
    .func(id);
}
</script>

This output was produced:
loading...
started 0
started 1
finished 0: date = Sat Jan 04 2014 12:03:06 GMT-0400 (AST), last = -1
finished 1: date = Sat Jan 04 2014 12:03:06 GMT-0400 (AST), last = -1
started 2
started 3
finished 2: date = Sat Jan 04 2014 12:03:08 GMT-0400 (AST), last = -1
finished 3: date = Sat Jan 04 2014 12:03:08 GMT-0400 (AST), last = -1

Which leads me to the conclusion that the server is stateless, at least from the point of view of the scripts running on it.

Friday, January 3, 2014

lazy initialization in JavaScript

I have a function that is not often used and has a long initialization step, when it is used it is used often, making it a very good candidate for lazy initialization. This function is in a Google Apps Script web application, so minimizing processing time is desirable.

Functions in JavaScript are assignable closures, so it is easy to write a lazy initializing function without any if statements.

var getImage = function(name) {
  var images = { };

  var files = MESSAGES_FOLDER.getFiles();
  while(files.hasNext()) {
    var file = files.next();
    images[file.getName()] = file.getDownloadUrl();
  }

  return (getImage = function(name) {
    return images[name];
  })(name);
}

After the initialization part of the function (making a map of file names to download urls) it is replaced by a new function that does not include the initialization, and that new function is called to get the result for the first use.

This is also an implementation of the State pattern.

Thursday, December 5, 2013

Simple and super practical note book

All the notebooks I have kept in my bag so far seemed lacking in little ways: I couldn't easily tuck things into them; sometimes finding the next blank page was a challenge; they consume a lot more space than I feel they need to; taking pages out of them quickly became unsightly.

So I started pondering how to make one where I could freely add and remove pages without having an effect on the feel of the note book.

I made the notebook from plastic canvas, and sewed it with strips of shopping bags.  The shell does not care about getting wet, but the pages inside do.


It is just larger than a quarter sheet of letter sized paper, so it is very easy to make new pages as I need them.


The pages are folded over a strip of plastic, so they can be completely rearranged, and anything thin that can be folded can be tucked between any of the pages.  I did not realize in advance that I can always be writing on the top few pages by just moving blank pages up to the top of the stack.  I had printed driving directions on the left side earlier in the week, and now they have been flipped over to be reused as note pages.

In the future I am going to try some graph paper or lined paper in here, but this is also working very well.

Thursday, November 21, 2013

Swing Components in HTML GUI in Java

I would like to use Swing components in a HTML based layout. Initially I tried to have empty elements with id attributes in the loaded text, but they were pruned from the internal representation, so I put text inside of them and replaced the whole element with the Swing component.

HtmlGui.java:
package ca.sarah_happy.sandbox;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.net.URL;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JSlider;
import javax.swing.JTextPane;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.html.HTMLDocument;

public class HtmlGui implements Runnable {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new HtmlGui());
    }

    private JTextPane viewer;
    private HTMLDocument document;

    @Override
    public void run() {
        try {
            viewer = new JTextPane();
            viewer.setEditable(false);
            viewer.setPreferredSize(new Dimension(400, 300));

            JFrame frame = new JFrame("screen");
            frame.setContentPane(new JScrollPane(viewer));
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

            URL screen = HtmlGui.class.getResource("screen.html");
            viewer.setPage(screen);
            document = (HTMLDocument) viewer.getDocument();
            viewer.addPropertyChangeListener("page", onLoad);

            frame.setVisible(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private JSlider slider;

    private PropertyChangeListener onLoad = new PropertyChangeListener() {
        @Override
        public void propertyChange(PropertyChangeEvent e) {
            JButton button = new JButton("Button");
            button.addActionListener(onButton);
            insertComponent("button", button);

            slider = new JSlider();
            slider.addChangeListener(onSlider);
            insertComponent("slider", slider);
        }
    };

    private ChangeListener onSlider = new ChangeListener() {
        @Override
        public void stateChanged(ChangeEvent e) {
            addOutput("changed slider: " + slider.getValue());
        }
    };

    private void insertComponent(String id, Component component) {
        Element e = document.getElement(id);
        viewer.setCaretPosition(e.getStartOffset());
        viewer.moveCaretPosition(e.getEndOffset());
        viewer.insertComponent(component);
    }

    private ActionListener onButton = new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent arg0) {
            addOutput("pressed button");
        }
    };

    private void addOutput(String text) {
        try {
            Element out = document.getElement("output");
            document.insertString(out.getEndOffset() - 1, text + "\n", null);
        } catch (BadLocationException ex) {
            ex.printStackTrace();
        }
    }
}

screen.html:
<html><body>
Some stuff...

<p><span id="button">button</span> <span id="slider">slider</span></p>

<p><i>Output:</i>
<div id="output"></div></p>
</body></html>

first HTML GUI in Java

Java GUI layout can be cumbersome, and HTML layout is less cumbersome. I wanted to have a screen in a Java application based on a HTML document.

For an initial attempt I made a screen with two buttons and an output area using a JEditorPane, the buttons are made from links and the output area is made from a document element.

HtmlText.java:
package ca.sarah_happy.sandbox;

import java.awt.Dimension;
import java.net.URL;

import javax.swing.JEditorPane;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.HyperlinkEvent;
import javax.swing.event.HyperlinkListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.html.HTMLDocument;

public class HtmlText implements Runnable {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new HtmlText());
    }

    private JEditorPane viewer;
    private HTMLDocument document;

    @Override
    public void run() {
        try {
            URL text = HtmlText.class.getResource("screen.html");
            viewer = new JEditorPane(text);
            viewer.setEditable(false);
            viewer.setPreferredSize(new Dimension(400, 300));

            JFrame frame = new JFrame("screen");
            frame.setContentPane(new JScrollPane(viewer));
            frame.pack();
            frame.setLocationByPlatform(true);
            frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

            document = (HTMLDocument) viewer.getDocument();
            viewer.addHyperlinkListener(onLink);

            frame.setVisible(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private HyperlinkListener onLink = new HyperlinkListener() {
        @Override
        public void hyperlinkUpdate(HyperlinkEvent e) {
            if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) {
                return;
            }

            String message = "pressed <" + e.getDescription() + ">";

            try {
                Element out = document.getElement("output");
                document.insertString(out.getEndOffset() - 1,
                        message + "\n", null);
            } catch (BadLocationException ex) {
                ex.printStackTrace();
            }
        }
    };
}

screen.html:
<html><body>
This is a test.
<p><a href="#link1">link 1</a>
<a href="#link2">link 2</a></p>

<p>output:
<div id="output"></div>
</p>
</body></html>

Friday, November 8, 2013

YouTube to iTunes

Sometimes I come across a video on YouTube that I would like to turn into a song in iTunes.

When thinking about puzzles, especially programming puzzles, often my thoughts are around what I have, what I want, and how I can bring the haves and wants closer to each other. As I put effort into the ways to bring the haves and wants together I end up with more haves and more options for wants. I am done when I find that I already have what I want.

I have a video on YouTube. I want a song in iTunes. iTunes can import mp3 files as songs, so I can also want a mp3 file.  I also have Chrome, and a freshly installed copy of Mac OSX Snow Leopard.

One option is YouTube to MP3 Converter, but it can only handle videos up to 20 minutes, and I want to convert a longer one.

I found a Chrome plugin that allows downloads of the YouTube media file that gets streamed to the browser, YouTube Options.  After installing that I can also have a .mp4 or .flv video file.

I have a script that I wrote a while ago that turns video files into mp3 files, but to run the script wants ffmpeg installed.  MacPorts has package for ffmpeg, but to install MacPorts it wants Xcode installed. Xcode is on the Snow Leopard Installation DVD, so I install it. Now I can install MacPorts, so I do. Now I can install ffmpeg, so I do (sudo port install ffmpeg) .

Now the script works, so I have what I wanted when I started, almost like magic.

A video on YouTube --> download the video from YouTube  --> convert the downloaded video to mp3 --> import the generated mp3 into iTunes --> a song in iTunes.

Thursday, November 7, 2013

fleece mittens

The mittens I used last winter shrank in the wash, so they did not fit well anymore, so I pondered how to make some out of fleece for a while, made a pattern by tracing another pair of mittens, and made myself some mittens.


They are symmetric so I don't need to figure out which one goes on which hand.

Monday, November 4, 2013

rainbow shoulder bag

One day a month or so ago I decided to make a new rainbow shoulder bag, one with a wider strap that would be more comfortable when I am wearing it for long periods of time. Today I got most of the work done.



The strap is filled with crocheted plastic bags giving it a sturdy padded structure. There are some loops of webbing to hang carabiners for holding things like my water bottle or hanging the bag off my backpack.  This bag is a bit larger than my previous one, I sized it using a mesh bag from the Dollarama.

In the future I will add a waterproof liner to keep the contents dry in the rain, and a zipper for easy closing.

Update: I didn't know at the time, but this bag also works as a backpack, with plenty of space.  I just clip a string across the bag and around the strap to make two spaces to put my arms, and because the strap is so wide it is very comfortable when it is weighted down.






Sunday, October 20, 2013

Facebook API Hello World

After working through the Facebook API Getting Started guide I decided to write an even simpler Hello World application that also displays some debugging information. This is my first Facebook Application.

facebook-hello.html:
<html>
<head><title>Facebook API Hello World</title></head>
<body>
<script type="text/javascript">
  window.fbAsyncInit = function() {
    // https://developers.facebook.com/docs/reference/javascript/FB.init/
    FB.init({
      appId: '222165227950143' /* registered application id */,
      status: true /* check the login status */,
      cookie: true /* set the session cookie */,
      xfbml: true /* enable social plugins */
    });

    // get the initial login status
    // https://developers.facebook.com/docs/reference/javascript/FB.getLoginStatus/
    FB.getLoginStatus(onLogin);
  };

  // Load the SDK Asynchronously
  (function(d, s, id){
    if (d.getElementById(id)) {return;}
    var js, fjs = d.getElementsByTagName(s)[0];
    js = d.createElement(s); js.id = id;
    js.src = "//connect.facebook.net/en_US/all.js";
    fjs.parentNode.insertBefore(js, fjs);
  }(document, 'script', 'facebook-jssdk'));

  function onLogin(response) {
    document.getElementById('onlogin-count').innerHTML++;
    document.getElementById('login-status').innerHTML = response.status;

    if(response.status === 'connected') {
      var uid = response.authResponse.userID;
      document.getElementById('user-id').innerHTML = uid;
      
      readName();
    }
  }

  function readName() {
    // https://developers.facebook.com/docs/reference/api/user/
    FB.api(
      'https://graph.facebook.com/me',
      'get',
      function(response) {
        document.getElementById("user-count").innerHTML++;
         
        var e = document.getElementById('user-name');
        if(!response) {
          e.innerHTML = '<i>no object</i>';
        } else if(response.error) {
          e.innerHTML = '<i>Error: ' + response.error.message + '</i>';
        } else {
          e.innerHTML = response.name;
        }
      }
    );
  }
</script>

<p>When we are connected to facebook, read the name of the current user and
display it here.  If you would like to read the name of the current user again
or before being connected to facebook, press the "read name" button.</p>

<p>
<!-- https://developers.facebook.com/docs/reference/plugins/login -->
<div><div 
  class="fb-login-button"
  data-scope=""
  data-onlogin="onLogin">
</div></div>

(Status: <span id="login-status"><i>???</i></span>)
(User ID: <span id="user-id"><i>???</i></span>)
(onLogin calls: <span id="onlogin-count">0</span>)
</p>

<p>
<input type="button" value="read name" onclick="readName()">
<span id="user-name"><i>???</i></span>
(readName calls: <span id="user-count">0</span>)
</p>

</body>
</html>

This is my first time working with the Facebook API. I like how all the API calls execute in the background, and not block the browser.

Friday, October 18, 2013

Escape HTML Text

Often when I am putting code examples up I have been manually escaping all the html tag characters to get it into the post. Now I have a piece of JavaScript to do it for me. I will present this as a short, self contained, correct, example.

escape.html:
<!DOCTYPE html>
<html><head><title>Escape HTML Text</title></head><body><form>

<div>text = <br>
<textarea id="text" rows="10" cols="80"></textarea></div>
<div><button type="button" onclick="setText(escapeHtml(getText()))">text = escapeHtml(text)</button></div>
<div><button type="button" onclick="setText(unescapeHtml(getText()))">text = unescapeHtml(text)</button></div>

<script type="text/javascript">
//<![CDATA[
    function getText() { return document.getElementById("text").value; }
    function setText(text) { document.getElementById("text").value = text; }
    function escapeHtml(text) {
        return text.replace(/&/g,"&amp;").replace(/"/g,"&quot;")
            .replace(/</g,"&lt;").replace(/>/g,"&gt;");
    }
    function unescapeHtml(text) {
        return text.replace(/&lt;/g,"<").replace(/&gt;/g,">")
            .replace(/&quot;/g,"\"").replace(/&amp;/g,"&");
    }
//]]>
</script>
</form></body></html>

Google Drive Hosting Hello World

Go to Google Drive

Create a folder, I called mine "public html", set the sharing to "Public on the web", in the details for the folder there should now be a link for hosting.  Upload whatever static files you would like into the shared folder.

Friday, October 11, 2013

Multi-media message save format and parsing

I have yet to have a phone that can save multi-media messages, so I took it upon myself to manually save them.

I use a very simple text based key-value format to save them. The keys are free text, and the values may contain arbitrary text, including multiple lines.

small
 value
multi-line
 line 1
 line 2
 line 3

Using this format I just choose some field names for the parts of the message, and separated messages with a field name of "done". Field names may be freely repeated.

type
 incoming
subject
 A Picture/Video Message!
date
 2012-11-27 23:38
from
 +15555555555
 Sender Name
to
 +15656565656
 My Name
text
 the text of the message goes here...
image
 imagejpeg_3.jpg
 5e35003a0e65df1065982c07587cf147ccb76b5d.jpg
done

the first line of a from or to is the number, the second line is the name of the contact. the first line of an image is the file name in the message, the second is the file name of the image in the directory. The other fields names are type, subject, date, text, and slide.

I broke the parsing of the records into usable data structures in Google Apps Script in two phases, the first one extracted the blocks of fields, and the other turned the fields into a data structure.

function forEachRecord(blob, body) {
  var fields = new Array();
  var field = { name: "", value: new Array() };
  
  var lines = blob.getDataAsString().split(/\r?\n/, -1);
  for(var lineIndex = 0; lineIndex < lines.length; lineIndex++) {
    var line = lines[lineIndex];

    // indented lines are the field value
    if(line.substr(0, 1) == " ") {
      field.value.push(line.substr(1));
      continue;
    }
    
    // blank lines are completely ignored
    if(line == "") {
      continue;
    }
    
    // starting a new field
    field = { name: line, value: new Array() };
    
    // records end when a field name of "done" is found
    if(line == "done") {
      body(fields);
      fields = new Array();
      continue;
    }

    // this field is part of the record
    fields.push(field);
  }
  
  if(fields.length) {
    body(fields);
  }
}

function forEachMessage(blob, body) {
  forEachRecord(blob, function(fields) {
    var m = {
      type: "",
      subject: "",
      date: "",
      from: new Array(),
      to: new Array(),
      body: new Array(),
    };
    
    for(var fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
      var f = fields[fieldIndex];
      
      if(f.name == "type") {
        m.type = f.value[0];
        continue;
      }
      
      if(f.name == "to") {
        m.to.push({ address: f.value[0], name: f.value[1] });
        continue;
      }
      
      if(f.name == "from") {
        m.from.push({ address: f.value[0], name: f.value[1] });
        continue;
      }
      
      if(f.name == "date") {
        m.date = f.value[0];
        continue;
      }
      
      if(f.name == "image") {
        m.body.push({ type: "image", name: f.value[0], file: f.value[1] });
        continue;
      }
      
      if(f.name == "text") {
        m.body.push({ type: "text", value: f.value.join("\n") });
        continue;
      }
      
      if(f.name == "slide") {
        m.body.push({ type: "slide" });
        continue;
      }
      
      // if we get here we found an unknown field
    }
    
    body(m);
  });
}

This looks similar to a loop in the function that uses it.

forEachMessage(file.getBlob(), function(message) {
  messages.push(message);
});

Turning the parsed messages into an easily human readable output is not difficult from here, and that was my overall objective.