Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Added new files

  • Loading branch information...
commit f7c0a73a6d36753187b793053535cf4cc0034fda 1 parent bfac020
Andrée Hansson authored February 25, 2012
2  .gitignore
... ...
@@ -0,0 +1,2 @@
  1
+config.js
  2
+.DS_Store
35  README.md
Source Rendered
... ...
@@ -0,0 +1,35 @@
  1
+socialhapy, the social developer irc bot
  2
+========================================
  3
+This IRC bot features a few useful modules and features that can help
  4
+developers in discussions, references etc. It'll pick up on URLs to
  5
+github commits, look up tweets and/or stream tweets to specific channels.
  6
+It also sports a spotify module that automatically picks up on spotify
  7
+URIs and lets everyone know what artist, track or album that was linked.
  8
+
  9
+How to get started
  10
+------------------
  11
+This project expects 0.4.0 of node.js or newer, but it'll probably work
  12
+work with lower versions if you fix the `node_modules` reference.
  13
+
  14
+1. Go into your Terminal application and do
  15
+   `git clone git@github.com:peol/socialhapy.git`
  16
+2. `cd socialhapy` and then initialize the required submodules needed by
  17
+   running `git submodule init` and `git submodule update --recursive`
  18
+3. Edit the `socialhapy/config.js` file to your liking
  19
+4. Run socialhapy by either running it with `node socialhapy.js` or by
  20
+   the recommended way, with `node-forever` or `nohup sh socialhapy.sh &`
  21
+
  22
+Create your own socialhapy module
  23
+---------------------------------
  24
+socialhapy has its own module system that leverages the `require` system but
  25
+demands that an API is returned with at least a `register` method in it, it's
  26
+also required that you add the module in `config.js`, see other entries under
  27
+the `modules` object.
  28
+
  29
+For a more in=depth guidance, see `examples/simple-module.js`.
  30
+
  31
+Roadmap
  32
+-------
  33
+There's not much but refactoring on the roadmap right now, but I'm happy to
  34
+merge in any optimizations/useful socialhapy modules that I see fit, just
  35
+send a pull request.
168  functions.js
... ...
@@ -0,0 +1,168 @@
  1
+var config = require('./config'),
  2
+    functions;
  3
+
  4
+module.exports = functions = {
  5
+    // Simple string format tool, {0}, {1} etc.
  6
+    // From http://stackoverflow.com/questions/610406/javascript-equivalent-to-printf-string-format/4673436#4673436
  7
+    format: function() {
  8
+        var args = Array.prototype.slice.call( arguments ),
  9
+            str = args.shift(),
  10
+            usePrefix = str === true;
  11
+
  12
+        if ( usePrefix ) {
  13
+            str = config.prefix + args.shift();
  14
+        }
  15
+
  16
+        return str.replace(/{(\d+)}/g, function(match, number) { 
  17
+            return typeof args[ number ] != 'undefined' ?
  18
+                args[ number ] :
  19
+                '{' + number + '}';
  20
+            });
  21
+    },
  22
+
  23
+    // Parse weeks, days, hours, minutes and seconds from seconds
  24
+    _duration: function(seconds, opts) {
  25
+        opts = opts || {
  26
+            asArray: false,
  27
+            filterValues: false,
  28
+            compress: false
  29
+        };
  30
+
  31
+        function subtract(div) {
  32
+            var v = Math.floor( seconds / div );
  33
+            seconds %= div;
  34
+        
  35
+            return v;
  36
+        }
  37
+
  38
+        var val = [
  39
+                [subtract( 31536000 ), ' year'],
  40
+                [subtract( 2628000 ), ' month'],
  41
+                [subtract( 604800 ), ' week'],
  42
+                [subtract( 86400 ), ' day'],
  43
+                [subtract( 3600 ), ' hour'],
  44
+                [subtract( 60 ), ' minute'],
  45
+                [seconds, ' second']
  46
+            ],
  47
+            i = 0,
  48
+            l = val.length,
  49
+            v;
  50
+
  51
+        for (; i < l; i += 1) {        
  52
+            v = val [ i ];
  53
+
  54
+            if ( opts.compress ) {
  55
+                // Take first char after space
  56
+                v[ 1 ] = v[ 1 ][ 1 ];
  57
+            }
  58
+            else {
  59
+                if ( val[ i ][ 0 ] !== 1 ) {
  60
+                    // Pluralize
  61
+                    val[ i ][ 1 ] += 's';
  62
+                }
  63
+            }
  64
+        }
  65
+
  66
+        if ( opts.filterValues ) {
  67
+            val = val.filter(function(i) {
  68
+                return i[ 0 ] > 0;
  69
+            });
  70
+        }
  71
+        
  72
+        if ( !opts.asArray ) {
  73
+            val = val
  74
+                .map(function(i){return i.join('');})
  75
+                .join(', ');
  76
+        }
  77
+        
  78
+        return opts.compress && !opts.asArray ?
  79
+            val.replace(/,/g,''):
  80
+            val;
  81
+    },
  82
+
  83
+    duration: function(secs, useSuffix, doCompress) {
  84
+        // Round the value if it's more than a minute, otherwise, round to three decimals
  85
+        secs = secs > 59 ? Math.round( secs ) : Math.round(secs * 1000) / 1000;
  86
+
  87
+        var dur = this._duration(secs, { filterValues: true, compress: doCompress, asArray: true }),
  88
+            i = 0,
  89
+            // Adjust this, by default we just want 2, e.g. "2 years, 3 months"
  90
+            l = Math.min(dur.length, 2),
  91
+            arr = [],
  92
+            str = '';
  93
+
  94
+
  95
+        for (; i < l; i += 1) {
  96
+            arr.push( dur[ i ].join('') );
  97
+        }
  98
+
  99
+        str = arr.join(', ');
  100
+
  101
+        return useSuffix ? str + ' ago' : str;
  102
+    },
  103
+
  104
+    // Extend an object with another object, just a shallow extend
  105
+    extend: function(extendee, extender) {
  106
+        var o;
  107
+
  108
+        for (o in extender) {
  109
+            extendee[ o ] = extender [ o ];
  110
+        }
  111
+    },
  112
+
  113
+    // Normalize a string for IRC output (remove unnecessary white space, new
  114
+    // lines etc.)
  115
+    normalize: function(str) {
  116
+        return str.replace(/\r\n|\n/g, '');
  117
+    },
  118
+
  119
+    // Helper for checking if `v1` has a value, and if the value is identical
  120
+    // to `v2`, defaults to `true`
  121
+    isMatchOrEmpty: function(v1, v2) {
  122
+        return typeof v1 != 'undefined' ?
  123
+            v1 === v2:
  124
+            true;
  125
+    },
  126
+
  127
+    // Checks whether a specific user is an admin or not
  128
+    isAdmin: function(user) {
  129
+        var adminUser = config.admins[ user.nick ],
  130
+            userMatch = adminUser && this.isMatchOrEmpty( adminUser.user, user.user ),
  131
+            hostMatch = adminUser && this.isMatchOrEmpty( adminUser.host, user.host );
  132
+
  133
+        return userMatch && hostMatch; 
  134
+    },
  135
+
  136
+    // Filters an array and returns a new array with unique values
  137
+    unique: function(arr) {
  138
+        var frequency = {},
  139
+            uniques = [];
  140
+
  141
+        arr.forEach(function(v) { frequency[ v ] = 0; });
  142
+
  143
+        uniques = arr.filter(function(val) {
  144
+            return ++frequency[ val ] == 1;
  145
+        });
  146
+
  147
+        return uniques;
  148
+    },
  149
+
  150
+    // Add a watcher to a specific jerk instance, it also checks if the watcher
  151
+    // is a admin-only command and validates the user
  152
+    addWatcher: function(jerk, w) {
  153
+        jerk.watch_for(w.pattern, function(m) {
  154
+            var that = this,
  155
+                isAdmin = functions.isAdmin( m.userData );
  156
+
  157
+            if ( w.adminOnly && !isAdmin ) {
  158
+                m.say(m.user + ': Sorry, admins only');
  159
+            }
  160
+            else {
  161
+                w.hollaback.call(that, m);
  162
+            }
  163
+        });
  164
+    }
  165
+};
  166
+ });
  167
+    }
  168
+};
57  modules/bitly.js
... ...
@@ -0,0 +1,57 @@
  1
+var functions = require('../functions'),
  2
+    Bitly = require('node-bitly').Bitly,
  3
+    urlp = require('url'),
  4
+    hollabacks,
  5
+    bitly;
  6
+
  7
+hollabacks = {
  8
+    bitly: function(m) {
  9
+        var url = m.match_data[ 1 ];
  10
+
  11
+        // Add http:// if it's not there
  12
+        if ( !/^http?(s)?:\/\//.test( url ) ) {
  13
+            url = 'http://' + url;
  14
+        }
  15
+
  16
+        bitly.createLink(url, function(bitlyURL) {
  17
+            if ( bitlyURL ) {
  18
+                m.say( functions.format(true, 'URL has been shortened: {0}', bitlyURL) );
  19
+            }
  20
+        });
  21
+    }
  22
+};
  23
+
  24
+bitly = module.exports = {
  25
+    _isLoaded: false,
  26
+    _sh: null,
  27
+    _bitly: null,
  28
+
  29
+    register: function(socialhapy) {
  30
+        this._isLoaded = true;
  31
+        this._sh = socialhapy;
  32
+
  33
+        var credentials = socialhapy.config.modules.bitly;
  34
+
  35
+        this._bitly = new Bitly(credentials.user, credentials.token);
  36
+
  37
+        functions.extend(socialhapy.watchers, this.watchers);
  38
+    },
  39
+
  40
+    createLink: function(url, hollaback) {
  41
+        this._bitly.shorten(url, function(result) {
  42
+            if ( !result.data.url ) {
  43
+                console.log('*** Warning: bit.ly module encountered an error while shortening a link: ' + result.status_txt);
  44
+            }
  45
+
  46
+            hollaback( result.data.url || '' );
  47
+        });
  48
+    },
  49
+
  50
+    watchers: {
  51
+        // .bitly <url>
  52
+        bitly: {
  53
+            pattern: /^\.bitly (.+?)$/i,
  54
+            hollaback: hollabacks.bitly
  55
+        }
  56
+    }
  57
+};
82  modules/core.js
... ...
@@ -0,0 +1,82 @@
  1
+var functions = require('../functions'),
  2
+    startDate = new Date(),
  3
+    watchers,
  4
+    core;
  5
+
  6
+watchers = {
  7
+    // .restart
  8
+    restart: {
  9
+        adminOnly: true,
  10
+        pattern: /^\.restart$/i,
  11
+        hollaback: function(m) {
  12
+            m.say( functions.format(true, 'Restarting... I was up for {0}', functions.duration( ( +new Date() - startDate ) / 1000 ) ) );
  13
+            // Note: This requires node-forever (forever-node?) or some sort
  14
+            // of auto-startup script
  15
+            // TODO: Send disconnect to the IRC server somehow, we'd need the jerk instance
  16
+            //       for that...
  17
+            setTimeout(function() {
  18
+                core._sh.jerk.quit("I'm just restarting!");
  19
+            }, 50);
  20
+
  21
+            setTimeout(function() {
  22
+                process.exit();
  23
+            }, 100);
  24
+        }
  25
+    },
  26
+
  27
+    // Add an idle channel temporary (handy when you want it to idle in a new
  28
+    // channel, but don't want to restart the bot - YOU DO NEED TO ADD THAT CHANNEL
  29
+    // MANUALLY IN THE CONFIG)
  30
+    join: {
  31
+        adminOnly: true,
  32
+        pattern: /^\.join (\#*)?(.+)$/i,
  33
+        hollaback: function(m) {
  34
+            var channel = '#' + m.match_data[ 2 ];
  35
+
  36
+            core._sh.jerk.join( channel );
  37
+
  38
+            m.say( functions.format(true, 'I (temporarily) joined {0}', channel) );
  39
+        }
  40
+    },
  41
+
  42
+    // Same as above, but leaves a channel
  43
+    part: {
  44
+        adminOnly: true,
  45
+        pattern: /^\.part (\#*)?(.+)$/i,
  46
+        hollaback: function(m) {
  47
+            var channel = '#' + m.match_data[ 2 ];
  48
+
  49
+            core._sh.jerk.part( channel );
  50
+
  51
+            m.say( functions.format(true, 'I (temporarily) left {0}', channel) );
  52
+        }
  53
+    },
  54
+
  55
+    // .uptime
  56
+    uptime: {
  57
+        pattern: /^\.uptime$/i,
  58
+        hollaback: function(m) {
  59
+            m.say( functions.format(true, 'I have been up for {0}', functions.duration( ( +new Date() - startDate ) / 1000 ) ) );
  60
+        }
  61
+    },
  62
+
  63
+    // .duration
  64
+    duration: {
  65
+        pattern: /^\.duration (\d+)$/i,
  66
+        hollaback: function(m) {
  67
+            m.say( functions.format(true, '{0}', functions.duration( parseFloat( m.match_data[ 1 ] ) )) );
  68
+        }
  69
+    }
  70
+};
  71
+
  72
+core = module.exports = {
  73
+    register: function(socialhapy) {
  74
+        functions.extend(this, {
  75
+            _isloaded: true,
  76
+            _sh: socialhapy
  77
+        });
  78
+
  79
+        functions.extend(socialhapy.watchers, this.watchers);
  80
+    },
  81
+    watchers: watchers
  82
+};
136  modules/github.js
... ...
@@ -0,0 +1,136 @@
  1
+var functions = require('../functions'),
  2
+    Github = require('node-github').GitHubApi,
  3
+    hollabacks,
  4
+    github;
  5
+
  6
+hollabacks = {
  7
+    commitLink: function(m) {
  8
+        var matches = m.match_data;
  9
+
  10
+        github.getCommit(matches[ 1 ], matches[ 2 ], matches[ 3 ], function(commit) {
  11
+            if ( !commit ) {
  12
+                github.handleError('no commit found', m);
  13
+                return;
  14
+            }
  15
+
  16
+            commit = commit.commit;
  17
+
  18
+            var fStr = 'Commit on \002{0}\002 by {1} ({2}): {3}',
  19
+                duration = functions.duration(( +new Date() / 1000 ) - ( +new Date( commit.committed_date ) / 1000 ), true, true),
  20
+                normalizedMessage = functions.normalize( commit.message );
  21
+
  22
+                m.say( functions.format(true, fStr, matches[ 2 ], commit.committer.login || commit.committer.name, duration, normalizedMessage) );
  23
+        });
  24
+    },
  25
+
  26
+    pullLink: function(m) {
  27
+        var matches = m.match_data;
  28
+
  29
+        github.getPullRequest(matches[ 1 ], matches[ 2 ], matches[ 3 ], function(pullReq) {
  30
+            if ( !pullReq ) {
  31
+                github.handleError('no pull request found', m);
  32
+                return;
  33
+            }
  34
+
  35
+            pullReq = pullReq.pull;
  36
+
  37
+            var fStr = 'Pull request on \002{0}\002 by {1} ({2}): {3}',
  38
+                duration = functions.duration((+new Date() - +new Date( pullReq.created_at)) / 1000, true, true);
  39
+            
  40
+            m.say( functions.format(true, fStr, matches[ 2 ], pullReq.user.login, duration, pullReq.title) );
  41
+     
  42
+// With #:
  43
+//            var fStr = 'Pull request #{0} on \002{1}\002 by {2}: {3}';
  44
+//            m.say( functions.format(true, fStr, pullReq.number, matches[ 2 ], pullReq.user.login, pullReq.title) );
  45
+        });
  46
+    },
  47
+
  48
+    issueLink: function(m) {
  49
+        var matches = m.match_data;
  50
+
  51
+        github.getIssue(matches[ 1 ], matches[ 2 ], matches[ 3 ], function(issue) {
  52
+            if ( !issue ) {
  53
+                github.handleError('no issue found', m);
  54
+                return;
  55
+            }
  56
+
  57
+            issue = issue.issue;
  58
+
  59
+            var fStr = 'Issue #{0} on \002{1}\002, reported by {2} ({3}): {4}',
  60
+                duration = functions.duration((+new Date() - +new Date( issue.created_at)) / 1000, true, true);
  61
+
  62
+            m.say( functions.format(true, fStr, issue.number, matches[ 2 ], issue.user, duration, issue.title) );
  63
+        });
  64
+    }
  65
+};
  66
+
  67
+github = module.exports = {
  68
+    register: function(socialhapy) {
  69
+        functions.extend(this, {
  70
+            _isloaded: true,
  71
+            _sh: socialhapy,
  72
+            _github: new Github( true )
  73
+        });
  74
+
  75
+        functions.extend(socialhapy.watchers, this.watchers);
  76
+    },
  77
+
  78
+    handleError: function(err, m) {
  79
+        var tmpl = '\002Github API:\002 {0}',
  80
+            errStr = 'internal error';
  81
+
  82
+        if ( typeof err === 'string' ) {
  83
+            errStr = err;
  84
+        }
  85
+        else {
  86
+            console.log( err );
  87
+        }
  88
+
  89
+        m.say( functions.format(true, tmpl, errStr) );
  90
+    },
  91
+
  92
+    getCommit: function(user, project, commit, hollaback) {
  93
+        var route = functions.format('commits/show/{0}/{1}/{2}', user, project, commit);
  94
+
  95
+        this.request(route, hollaback);
  96
+   },
  97
+
  98
+    getPullRequest: function(user, project, id, hollaback) {
  99
+        var route = functions.format('pulls/{0}/{1}/{2}', user, project, id);
  100
+
  101
+        this.request(route, hollaback);
  102
+    },
  103
+
  104
+    getIssue: function(user, project, issueNo, hollaback) {
  105
+        var route = functions.format('issues/show/{0}/{1}/{2}', user, project, issueNo);
  106
+
  107
+        this.request(route, hollaback);
  108
+    },
  109
+
  110
+    request: function(route, hollaback) {
  111
+        this._github.get(route, null, null, function(err, data) {
  112
+            // TODO: Add error handling
  113
+            hollaback( data );
  114
+        });
  115
+    },
  116
+
  117
+    watchers: {
  118
+        // Commit links
  119
+        commitLink: {
  120
+            pattern: /github\.com\/(.+?)\/(.+?)\/commit\/([a-z0-9]+)/i,
  121
+            hollaback: hollabacks.commitLink
  122
+        },
  123
+
  124
+        // Pull request links
  125
+        pullLink: {
  126
+            pattern: /github\.com\/(.+?)\/(.+?)\/pull\/(\d+)/i,
  127
+            hollaback: hollabacks.pullLink
  128
+
  129
+        },
  130
+
  131
+        issueLink: {
  132
+            pattern: /github\.com\/(.+?)\/(.+?)\/issues\/(\d+)/i,
  133
+            hollaback: hollabacks.issueLink
  134
+        }
  135
+   }
  136
+};
222  modules/spotify.js
... ...
@@ -0,0 +1,222 @@
  1
+var functions = require('../functions'),
  2
+    urlShortener = require('./bitly'),
  3
+    spotifyAPI = require('node-spotify'),
  4
+    hollabacks,
  5
+    parsers,
  6
+    helpers,
  7
+    spotify;
  8
+
  9
+hollabacks = {
  10
+    def: function(m) {
  11
+        var isSearch = m.match_data[ 0 ].indexOf('spotifind') >= 0,
  12
+            method = isSearch ? 'search' : 'lookup',
  13
+            type = m.match_data[ 1 ],
  14
+            str = m.match_data[ 2 ],
  15
+            apiOptions = { type: type, query: str, id: str };
  16
+
  17
+        spotifyAPI[ method ](apiOptions, function(err, data) {
  18
+            var str = '';
  19
+
  20
+            if ( err ) {
  21
+                console.log( err );
  22
+                str = 'An internal error while fetching Spotify data occurred';
  23
+            }
  24
+            else {
  25
+                data = parsers.data(type, data);
  26
+
  27
+                if ( !data || !data[ 0 ] ) {
  28
+                    str = 'No matches in the Spotify library';
  29
+                }
  30
+            }
  31
+
  32
+            if ( str ) {
  33
+                // Means that we either have an error on our hands, or
  34
+                // just no data
  35
+                m.say( functions.format(true, str) );
  36
+            }
  37
+            else {
  38
+                console.log(data);
  39
+                str = parsers[ type ].apply(parsers, data);
  40
+
  41
+                if ( isSearch || m.match_data[ 0 ].indexOf('spotify:') !== -1 ) {
  42
+                    // Create link from Spotify href (spotify:type:id -> type/id)
  43
+                    urlShortener.createLink('http://open.spotify.com/' + data[ 0 ].href.split(':').slice(1).join('/'), function(url) {
  44
+                        m.say( functions.format(str + ' {0}{1}', spotify._sh.config.prefix, url) );
  45
+                    });
  46
+                }
  47
+                else {
  48
+                    m.say( str );
  49
+                }
  50
+            }
  51
+
  52
+       });
  53
+    }
  54
+};
  55
+
  56
+parsers = {
  57
+    track: function(track, notAvailableIn, skippedTracks) {
  58
+        var artists = track.artists.map(function(a) { return a.name; }).join(', '),
  59
+            popularity = helpers.generatePopularity( track.popularity ),
  60
+            tmpl = '\002{0} - {1}\002 (Popularity: {2}';
  61
+
  62
+        if ( notAvailableIn.length ) {
  63
+            tmpl += functions.format(', N/A in \002{0}\002', notAvailableIn.join(', '));
  64
+        }
  65
+
  66
+        if ( skippedTracks ) {
  67
+            tmpl += functions.format(', skipped {0} tracks', skippedTracks);
  68
+        }
  69
+
  70
+        return functions.format(true, tmpl + ')', artists, track.name, popularity);
  71
+    },
  72
+
  73
+    album: function(album, notAvailableIn, skippedAlbums) {
  74
+        var artists = album.artist || album.artists.map(function(a) { return a.name; }).join(', '),
  75
+            popularity = helpers.generatePopularity( album.popularity ),
  76
+            tmpl = '\002{0} - {1}\002 (Popularity: {2}';
  77
+
  78
+	if ( album.released ) {
  79
+	    tmpl += functions.format(', released in {0}', album.released);
  80
+	}
  81
+
  82
+        if ( notAvailableIn.length ) {
  83
+            tmpl += functions.format(', N/A in \002{0}\002', notAvailableIn.join(', '));
  84
+        }
  85
+
  86
+        if ( skippedAlbums ) {
  87
+            tmpl += functions.format(', skipped {0} albums', skippedAlbums);
  88
+        }
  89
+
  90
+       return functions.format(true, tmpl + ')', artists, album.name, popularity);
  91
+    },
  92
+
  93
+    artist: function(artist) {
  94
+        var popularity = helpers.generatePopularity( artist.popularity ),
  95
+            tmplAlbums = functions.format(', has {1} album(s) available in the Spotify library', artist.albums),
  96
+            tmplPopularity = ' (Popularity: {2})',
  97
+            tmpl = '\002{0}\002';
  98
+
  99
+        if ( artist.albums ) {
  100
+            tmpl += tmplAlbums;
  101
+        }
  102
+
  103
+        if ( artist.popularity ) {
  104
+            tmpl += tmplPopularity;
  105
+        }
  106
+
  107
+        return functions.format(true, tmpl, artist.name, artist.albums && artist.albums.length, popularity);
  108
+    },
  109
+
  110
+    data: function(type, data) {
  111
+        var plural = type + 's',
  112
+            t = {};
  113
+
  114
+        if ( !data[ plural ] ) {
  115
+            t[ plural ] = [ data[ type ] ];
  116
+            data = t;
  117
+        }
  118
+
  119
+        data = helpers.findCompatible( data[ plural ] );
  120
+
  121
+        return data;
  122
+    }
  123
+};
  124
+
  125
+helpers = {
  126
+    findCompatible: function(data) {
  127
+        var i = 0,
  128
+            l = data.length,
  129
+            bestMatch = null,
  130
+            o,
  131
+            avail;
  132
+
  133
+        for (; i < l; i += 1) {
  134
+            o = data[ i ];
  135
+
  136
+            if ( !o ) {
  137
+                // No usuable data found
  138
+                continue;
  139
+            }
  140
+
  141
+            avail = this.checkAvailability( o.availability || o.album && o.album.availability || { territories: '' } );
  142
+
  143
+            if ( !bestMatch || bestMatch[ 0 ].length > avail.length ) {
  144
+                bestMatch = [o, avail, i];
  145
+
  146
+                if ( !avail.length ) {
  147
+                    // We found a perfect match, no need to continue
  148
+                    break;
  149
+                }
  150
+            }
  151
+        }
  152
+
  153
+        return bestMatch;
  154
+    },
  155
+
  156
+    checkAvailability: function(terr) {
  157
+        terr = terr.territories.split(' ');
  158
+
  159
+        var compat = spotify.config.compatible,
  160
+            i = 0,
  161
+            l = compat.length,
  162
+            absent = [];
  163
+
  164
+        if ( terr[ 0 ] !== '' ) {
  165
+            // Only loop through if we actually have data,
  166
+            // or it'll return false-positives
  167
+            for (; i < l; i += 1) {
  168
+                if ( terr.indexOf( compat[ i ] ) === -1 ) {
  169
+                    absent.push( compat[ i ] );
  170
+                }
  171
+            }
  172
+        }
  173
+
  174
+        return absent;
  175
+    },
  176
+
  177
+    generatePopularity: function(index) {
  178
+        var str = '',
  179
+            i = 0,
  180
+            l = 10;
  181
+
  182
+        index = Math.round( parseFloat( index ) * 10 );
  183
+
  184
+        for (; i < l; i += 1) {
  185
+            str += i < index ? '▮' : '▯';
  186
+        }
  187
+
  188
+        return str;
  189
+    }
  190
+};
  191
+
  192
+spotify = module.exports = { 
  193
+    register: function(socialhapy) {
  194
+        functions.extend(this, {
  195
+            _isloaded: true,
  196
+            _sh: socialhapy,
  197
+            config: socialhapy.config.modules.spotify
  198
+        });
  199
+
  200
+        functions.extend(socialhapy.watchers, this.watchers);
  201
+    },
  202
+
  203
+    watchers: {
  204
+        // .spotifind <type>:<search string>
  205
+        spotifind: {
  206
+            pattern: /\.spotifind (track|album|artist):(.+?)$/i,
  207
+            hollaback: hollabacks.def
  208
+        },
  209
+
  210
+        // URI links
  211
+        uri: {
  212
+            pattern: /spotify:(track|artist|album):([a-zA-Z0-9]+)/i,
  213
+            hollaback: hollabacks.def 
  214
+        },
  215
+ 
  216
+        // URL links
  217
+        url: {
  218
+            pattern: /open\.spotify\.com\/(track|artist|album)\/([a-zA-Z0-9]+)/i,
  219
+            hollaback: hollabacks.def 
  220
+        }
  221
+    }
  222
+};
43  modules/twitter-poll.js
... ...
@@ -0,0 +1,43 @@
  1
+!function() {
  2
+	var targetHost = "oksoclap.com",
  3
+	    targetPath = "",
  4
+		//targetPath = "/ep/pad/export/SOME-PAD-YOU-CREATED/latest?format=txt",
  5
+		http = require('http');
  6
+
  7
+	function pollTwitterList(callback) {
  8
+	    if (targetPath === '') {
  9
+	        callback( [] );
  10
+	        return;
  11
+	    }
  12
+	    
  13
+		var chunks = "",
  14
+		    server = http.createClient(80, 'www.' + targetHost),
  15
+		    request = server.request('GET', targetPath, { 'host': targetHost });
  16
+
  17
+		request.end();
  18
+
  19
+		request.on('response', function (response) {
  20
+			response.setEncoding('utf8');
  21
+
  22
+			response.on('data', function (chunk) {
  23
+				chunks += chunk;
  24
+			});
  25
+
  26
+			response.on('end', function() {
  27
+				chunks = chunks
  28
+					// Add commas between each person to follow
  29
+					.replace(/\n+/g, ',')
  30
+
  31
+					// Remove any whitespace
  32
+					.replace(/\s*/g, '')
  33
+
  34
+					// Remove last comma
  35
+					.slice(0, -1);
  36
+
  37
+					callback( chunks.split(',') );
  38
+			});
  39
+		});
  40
+	}
  41
+
  42
+	exports.poll = pollTwitterList;
  43
+}();
171  modules/twitter.js
... ...
@@ -0,0 +1,171 @@
  1
+var functions = require('../functions'),
  2
+    urlShortener = require('./bitly'),
  3
+    Twitter = require('ntwitter'),
  4
+    hollabacks,
  5
+    stream,
  6
+    twitter;
  7
+
  8
+hollabacks = {
  9
+    tweet: function(m) {
  10
+        var target = m.match_data[ m.match_data.length - 1 ],
  11
+            method = +target ? 'getStatus' : 'getLatestTweet';
  12
+
  13
+        twitter[ method ](target, function(err, data) {
  14
+            if ( err || !data ) {
  15
+                if ( err.statusCode === 401 ) {
  16
+                    // 401's are protected tweets 99/100 times
  17
+                    m.say( functions.format(true, '\002{0}\002 has protected tweets', target) );
  18
+                }
  19
+                else if ( err.statusCode === 404 ) {
  20
+                    // 404's are twitter status errors (protected or non-existing)
  21
+                    m.say( functions.format(true, "Could not find the tweet with id \002{0}\002", target) );
  22
+                }
  23
+                else if ( !data ) {
  24
+                    // User was not found
  25
+                    m.say( functions.format(true, "Twitter user was not found, or no public tweets available") );
  26
+                }
  27
+                else {
  28
+                    console.log('Twitter error:', err.stack);
  29
+                    m.say( functions.format(true, 'Unknown twitter error encountered. The error has been logged.') );
  30
+                }
  31
+
  32
+                return;
  33
+            }
  34
+
  35
+            twitter.createTweet(true, data.user.screen_name, data.text, data.id_str, function(msg) {
  36
+                m.say( msg );
  37
+            });
  38
+        });
  39
+    }
  40
+};
  41
+
  42
+stream = {
  43
+    _stream: null,
  44
+
  45
+    start: function() {
  46
+        if ( this._stream ) {
  47
+            this._stream.destroy();
  48
+        }
  49
+
  50
+        this.getIds(twitter.config.users, this._bind);
  51
+    },
  52
+
  53
+    _bind: function(userIds) {
  54
+        twitter._twitter.stream('statuses/filter', { follow: userIds.join(',') }, function(strm) {
  55
+            console.log('Twitter stream started, following: ' + userIds);
  56
+            stream._stream = strm;
  57
+
  58
+            strm.on('data', stream.receive);
  59
+
  60
+            strm.on('error', function(err) {
  61
+                console.log('Twitter stream error: ' + err);
  62
+            });
  63
+
  64
+            strm.on('end', function() {
  65
+		console.log(strm);
  66
+                console.log('Twitter stream ended, trying to start it up again in 60 seconds...');
  67
+                setTimeout(stream.start.bind( stream ), 60000);
  68
+            });
  69
+        });
  70
+    },
  71
+
  72
+    receive: function(data) {
  73
+        if ( data.friends ) {
  74
+            // Ignore initial call
  75
+            return;
  76
+        }
  77
+
  78
+        var user = data.user.screen_name,
  79
+            o,       
  80
+            channels = twitter.config.streamChannels,
  81
+            chan;
  82
+
  83
+        if (/^RT/i.test( data.text ) || data.in_reply_to_user_id_str) {
  84
+            // Ignore RT's (very spammy)
  85
+            return;
  86
+        }
  87
+
  88
+        twitter.createTweet(true, user, data.text, data.id_str, function(msg) {
  89
+            for (o in channels) {
  90
+                chan = channels[ o ];
  91
+                if ( !chan.length || chan.indexOf( user.toLowerCase() ) !== -1 ) {
  92
+                    twitter._sh.jerk.say(o, msg);
  93
+                }
  94
+            }
  95
+        });
  96
+    },
  97
+
  98
+    getIds: function(names, hollaback) {
  99
+        twitter._twitter.get('/users/lookup.json', { screen_name: names.join(',') }, function(err, data) {
  100
+            var ids = [],
  101
+                i = 0,
  102
+                l = data && data.length || 0;
  103
+
  104
+            for (; i < l; i += 1) {
  105
+                ids.push( data[ i ].id_str );
  106
+            }
  107
+
  108
+            hollaback( ids );
  109
+        });
  110
+    }
  111
+};
  112
+
  113
+twitter = module.exports = {
  114
+    register: function(socialhapy) {
  115
+        var config = socialhapy.config.modules.twitter;
  116
+
  117
+        functions.extend(this, {
  118
+            _isloaded: true,
  119
+            _sh: socialhapy,
  120
+            config: config,
  121
+            _twitter: new Twitter( config )
  122
+        });
  123
+
  124
+        stream.start();
  125
+
  126
+        functions.extend(socialhapy.watchers, this.watchers); 
  127
+    },
  128
+
  129
+    createTweet: function(includeURL, screenName, message, id, hollaback) {
  130
+        var tmpl = functions.format(true, 'Tweet from \002{0}\002: {1}', screenName, message);
  131
+
  132
+        if ( includeURL ) {
  133
+            urlShortener.createLink(functions.format('https://twitter.com/{0}/status/{1}', screenName, id), function(url) {
  134
+                tmpl += ' ' + twitter._sh.config.prefix + url;
  135
+                hollaback( tmpl );
  136
+            });
  137
+        }
  138
+        else {
  139
+            hollaback( tmpl );
  140
+        }
  141
+    },
  142
+
  143
+    getLatestTweet: function(user, hollaback) {
  144
+        this._twitter.get('/statuses/user_timeline.json', { count: 1, screen_name: user, include_entities: true }, function(err, data) {
  145
+            hollaback(err, data && data[ 0 ]);
  146
+        });
  147
+    },
  148
+
  149
+    getStatus: function(id, hollaback) {
  150
+        this._twitter.get(functions.format('/statuses/show/{0}.json', id), { include_entities: true }, hollaback);
  151
+    },
  152
+
  153
+   watchers: {
  154
+        //.tweet <user/status id>
  155
+        tweet: {
  156
+            pattern: /^\.tweet (.+?)$/i,
  157
+            hollaback: hollabacks.tweet
  158
+        },
  159
+
  160
+        // Matches twitter.com normal links
  161
+        tweetLinkWithStatus: {
  162
+            pattern: /twitter\.com\/(?:#!\/)?(.+?)\/status(?:es)?\/(\d+)/i,
  163
+            hollaback: hollabacks.tweet
  164
+        },
  165
+
  166
+        tweetLinkWithUser: {
  167
+            pattern: /twitter\.com\/(?:#!\/)?(\w+)\/?(?=\s|$)/i,
  168
+            hollaback: hollabacks.tweet
  169
+        }
  170
+    }
  171
+};
1  node_modules/Jerk
... ...
@@ -0,0 +1 @@
  1
+Subproject commit f18cd004e80bafe56a18d38aca06901ccd062c23
1  node_modules/node-bitly
... ...
@@ -0,0 +1 @@
  1
+Subproject commit 15e6743d80129806ebd0c1f2c420c77930db8d0e
1  node_modules/node-github
... ...
@@ -0,0 +1 @@
  1
+Subproject commit fc9993f5a298bda0a16cf54b52b832fa12701a0f
1  node_modules/node-spotify
... ...
@@ -0,0 +1 @@
  1
+Subproject commit 131d0aa26863bf89a483f0487f17cfb0c93d8507
1  node_modules/ntwitter
... ...
@@ -0,0 +1 @@
  1
+Subproject commit bbbc55521ae7cf72a18359289e47cdd52a64bc41
73  socialhapy.js
... ...
@@ -0,0 +1,73 @@
  1
+var Jerk = require('Jerk'),
  2
+    config = require('./config'),
  3
+    functions = require('./functions'),
  4
+    jerkInstance = null,
  5
+    socialhapy = {
  6
+        config: config,
  7
+        watchers: {},
  8
+        jerk: null
  9
+    },
  10
+    watchers = socialhapy.watchers;
  11
+
  12
+function loadModules() {
  13
+    var modules = config.modules,
  14
+        o, m, module;
  15
+
  16
+    function warn(name, str) {
  17
+        console.log( functions.format('*** Warning: The `{0}` module was not loaded, {1}', name, str) );
  18
+    }
  19
+
  20
+    for (o in modules) {
  21
+        m = modules[ o ];
  22
+
  23
+        if ( !m.enabled ) {
  24
+            continue;
  25
+        }
  26
+
  27
+        try { module = require('./modules/' + o); }
  28
+        catch (e) {
  29
+            warn(o, 'error while pulling in the module: ' + e);
  30
+            continue;
  31
+        }
  32
+
  33
+        if ( typeof module.register != 'undefined' ) {
  34
+            module.register( socialhapy );
  35
+        }
  36
+        else {
  37
+            warn(o, 'missing `invoke` function');
  38
+        }
  39
+    }
  40
+}
  41
+
  42
+function initialize() {
  43
+    var chans = config.channels,
  44
+        ircChans = [],
  45
+        o;
  46
+        
  47
+    loadModules();
  48
+
  49
+    if ( config.debugMode ) {
  50
+        // Skip all other channels
  51
+        chans = { debug: chans.debug };
  52
+    }
  53
+
  54
+    // Prepare channel array
  55
+    for (o in chans) {
  56
+         ircChans = ircChans.concat( chans[ o ] );   
  57
+    }
  58
+
  59
+    config.irc.channels = functions.unique( ircChans );
  60
+
  61
+    // Connect to IRC
  62
+    socialhapy.jerk = jerkInstance = Jerk().connect( config.irc );     
  63
+
  64
+    for (o in watchers) {
  65
+        functions.addWatcher( jerkInstance, watchers[ o ] );
  66
+    }
  67
+}
  68
+
  69
+initialize();
  70
+ct( config.irc );
  71
+}
  72
+
  73
+initialize();

0 notes on commit f7c0a73

Please sign in to comment.
Something went wrong with that request. Please try again.