├── .gitignore
├── .travis.yml
├── LICENSE
├── README.rst
├── bin
└── twtxt.js
├── config.example
├── lib
├── cli.js
├── config.js
├── file.js
├── helper.js
└── parser.js
├── package.json
├── test
├── cli.js
├── config.js
├── file.js
├── helper.js
└── parser.js
└── twtxt.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .coverage
3 | *.pyc
4 | *.egg-info
5 | *.pex
6 | .cache
7 | .idea
8 | build
9 | dist
10 | venv
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.12
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 buckket
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 |
2 | twtxt
3 | ~~~~~
4 | |gitter| |license|
5 |
6 | **twtxt** is a decentralised, minimalist microblogging service for hackers.
7 |
8 | So you want to get some thoughts out on the internet in a convenient and slick way while also following the gibberish of others? Instead of signing up at a closed and/or regulated microblogging platform, getting your status updates out with twtxt is as easy as putting them in a publicly accessible text file. The URL pointing to this file is your identity, your account. twtxt then tracks these text files, like a feedreader, and builds your unique timeline out of them, depending on which files you track. The format is simple, human readable, and integrates well with UNIX command line utilities.
9 |
10 |
11 | |demo|
12 |
13 | **tl;dr**: twtxt is a CLI tool, as well as a format specification for self-hosted flat file based microblogging.
14 |
15 | -----
16 |
17 | .. contents::
18 | :local:
19 | :depth: 1
20 | :backlinks: none
21 |
22 | -----
23 |
24 | Features
25 | --------
26 | - A beautiful command-line interface thanks to click.
27 | - Asynchronous HTTP requests
28 | - Integrates well with existing tools (scp, cut, echo, date, etc.) and your shell.
29 | - Don’t like the official client? Tweet using ``echo -e "`date -Im`\tHello world!" >> twtxt.txt``!
30 |
31 | Installation
32 | ------------
33 |
34 | Release version:
35 | ================
36 | 1) Make sure that you have the latest npm and node
37 |
38 | 2) Install this package using npm:
39 |
40 | .. code::
41 |
42 | $ npm install -g twtxt
43 |
44 | 3) Run ``twtxt quickstart``. :)
45 |
46 | Usage
47 | -----
48 | twtxt features an excellent command-line interface thanks to `click `_. Don’t hesitate to append ``--help`` or call commands without arguments to get information about all available commands, options and arguments.
49 |
50 | Here are a few of the most common operations you may encounter when using twtxt:
51 |
52 | Follow a source:
53 | ================
54 |
55 | .. code::
56 |
57 | $ twtxt follow bob http://bobsplace.xyz/twtxt
58 | ✓ You’re now following bob.
59 |
60 | List all sources you’re following:
61 | ==================================
62 |
63 | .. code::
64 |
65 | $ twtxt following
66 | ➤ alice @ https://example.org/alice.txt
67 | ➤ bob @ http://bobsplace.xyz/twtxt
68 |
69 | Unfollow a source:
70 | ==================
71 |
72 | .. code::
73 |
74 | $ twtxt unfollow bob
75 | ✓ You’ve unfollowed bob.
76 |
77 | Post a status update:
78 | =====================
79 |
80 | .. code::
81 |
82 | $ twtxt tweet "Hello, this is twtxt!"
83 |
84 | View your timeline:
85 | ===================
86 |
87 | .. code::
88 |
89 | $ twtxt timeline
90 |
91 | ➤ bob (5 minutes ago):
92 | This is my first "tweet". :)
93 |
94 | ➤ alice (2 hours ago):
95 | I wonder if this is a thing?
96 |
97 | Configuration
98 | -------------
99 | twtxt uses a simple INI-like configuration file. It’s recommended to use ``twtxt quickstart`` to create it. On Linux twtxt checks ``~/.config/twtxt/config`` for it’s configuration. Consult `get_app_dir `_ to find out the config directory for other operating systems.
100 |
101 | Note: The npm version uses json instead of an ini.
102 |
103 | Here’s an example ``conf`` file, showing every currently supported option:
104 |
105 | .. code::
106 |
107 | {
108 | "user": "melvincarvalho",
109 | "twtfile": "/home/user/twtxt.txt",
110 | "limit_timeline": "20",
111 | "following": [
112 | {
113 | "user": "twtxt",
114 | "uri": "https://buckket.org/twtxt_news.txt"
115 | }
116 | ]
117 | }
118 |
119 |
120 | [twtxt] section:
121 | ================
122 | +-------------------+-------+------------+---------------------------------------------------+
123 | | Option: | Type: | Default: | Help: |
124 | +===================+=======+============+===================================================+
125 | | nick | TEXT | | your nick, will be displayed in your timeline |
126 | +-------------------+-------+------------+---------------------------------------------------+
127 | | twtfile | PATH | | path to your local twtxt file |
128 | +-------------------+-------+------------+---------------------------------------------------+
129 | | limit_timeline | INT | 20 | limit amount of tweets shown in your timeline |
130 | +-------------------+-------+------------+---------------------------------------------------+
131 | | post_tweet_hook | TEXT | | command to be executed after tweeting |
132 | +-------------------+-------+------------+---------------------------------------------------+
133 |
134 | ``post_tweet_hook`` is very useful if you want to push your twtxt file to a remote (web) server. Check the example above tho see how it’s used with ``scp``.
135 |
136 | [followings] section:
137 | =====================
138 | This section holds all your followings as nick, URL pairs. You can edit this section manually or use the ``follow``/``unfollow`` commands of twtxt for greater comfort.
139 |
140 | Format specification
141 | --------------------
142 | The central component of sharing information, i.e. status updates, with twtxt is a simple text file containing all the status updates of a single user. One status per line, each of which is equipped with an ISO 8601 date/time string followed by a TAB character (\\t) to separate it from the actual text. A specific ordering of the statuses is not mandatory.
143 |
144 | The file must be encoded with UTF-8 and must use LF (\\n) as line separators.
145 |
146 | A status should consist of up to 140 characters, longer status updates are technically possible but discouraged. twtxt will warn the user if a newly composed status update exceeds this limit, and it will also shorten incoming status updates by default. Also note that a status may not contain any control characters.
147 |
148 | Take a look at this example file:
149 |
150 | .. code::
151 |
152 | 2016-02-04T13:30+01 You can really go crazy here! ┐(゚∀゚)┌
153 | 2016-02-01T11:00+01 This is just another example.
154 | 2015-12-12T12:00+01 Fiat lux!
155 |
156 | Contributions
157 | -------------
158 | - A web-based directory of twtxt users by `reednj `_: http://twtxt.reednj.com/
159 | - A web-based directory of twtxt users by `xena `_: https://twtxtlist.cf
160 | - A web-based twtxt feed hoster for the masses by `plomlompom `_: https://github.com/plomlompom/htwtxt
161 | - A twitter-to-twtxt converter in node.js by `DracoBlue `_: https://gist.github.com/DracoBlue/488466eaabbb674c636f
162 |
163 | License
164 | -------
165 | twtxt is released under the MIT License. See the bundled LICENSE file for details.
166 |
167 |
168 | .. |gitter| image:: https://img.shields.io/gitter/room/buckket/twtxt.svg?style=flat
169 | :target: https://gitter.im/buckket/twtxt
170 | :alt: Chat on gitter
171 |
172 | .. |license| image:: https://img.shields.io/badge/license-MIT-blue.svg?style=flat
173 | :target: https://raw.githubusercontent.com/buckket/twtxt/master/LICENSE
174 | :alt: Package license
175 |
176 | .. |demo| image:: https://asciinema.org/a/1w2q3suhgrzh2hgltddvk9ot4.png
177 | :target: https://asciinema.org/a/1w2q3suhgrzh2hgltddvk9ot4
178 | :alt: Demo
179 |
--------------------------------------------------------------------------------
/bin/twtxt.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // requires
4 | var fs = require('fs');
5 | var program = require('commander');
6 | var request = require('request');
7 | var debug = require('debug')('twtxt');
8 | var childProcess = require('child_process');
9 | var readline = require('readline');
10 | var moment = require('moment');
11 |
12 | var tweet = require("../lib/cli").tweet;
13 | var follow = require("../lib/cli").follow;
14 | var unfollow = require("../lib/cli").unfollow;
15 | var following = require("../lib/cli").following;
16 | var quickstart = require("../lib/cli").quickstart;
17 | var timeline = require("../lib/cli").timeline;
18 |
19 |
20 | var error = debug('app:error');
21 |
22 | // defaults
23 | var default_limit_timeline = 20;
24 |
25 | /**
26 | * run as bin
27 | */
28 | function bin() {
29 | var MAXCHARS = 140;
30 | var user;
31 | var uri;
32 |
33 | program
34 | .arguments(' [arg] [uri]')
35 | .option('-c, --config ', 'Specify a custom config file location.')
36 | .option('-v, --verbose', 'Enable verbose output for deubgging purposes')
37 | .option('--version', 'Shows the version and exit.')
38 | .action(function (cmd, arg, uri) {
39 | cmdValue = cmd;
40 | argValue = arg;
41 | uriValue = uri;
42 | });
43 |
44 | program.parse(process.argv);
45 |
46 | if (typeof cmdValue === 'undefined') {
47 | console.error('no command given!');
48 | process.exit(1);
49 | }
50 | if (program.verbose) {
51 | process.env.DEBUG = 'twtxt';
52 | debug = console.log;
53 | }
54 | debug('command:', cmdValue);
55 | debug('arg: ', argValue || "no arg given");
56 | debug('uri: ', uriValue || "no uri given");
57 | debug('verbose: ' + program.verbose);
58 |
59 | if (cmdValue === 'tweet') {
60 | var description = argValue;
61 |
62 | if (!description) {
63 | console.error('Usage: twtxt tweet ');
64 | process.exit(-1);
65 | }
66 |
67 | if (description && description.length > MAXCHARS) {
68 | console.error('Maximum tweet length : ' + MAXCHARS);
69 | process.exit(-1);
70 | }
71 |
72 | tweet(description);
73 | } else if (cmdValue === 'follow') {
74 | user = argValue;
75 | uri = uriValue;
76 |
77 | if (!user) {
78 | console.error('Usage: twtxt follow ');
79 | process.exit(-1);
80 | }
81 |
82 | if (!uri) {
83 | console.error('Usage: twtxt follow ');
84 | process.exit(-1);
85 | }
86 |
87 | follow(user, uri);
88 |
89 | } else if (cmdValue === 'unfollow') {
90 | user = argValue;
91 |
92 | if (!user) {
93 | console.error('Usage: twtxt unfollow ');
94 | process.exit(-1);
95 | }
96 |
97 | unfollow(user);
98 |
99 | } else if (cmdValue === 'following') {
100 | following();
101 |
102 | } else if (cmdValue === 'timeline') {
103 | timeline();
104 |
105 | } else if (cmdValue === 'quickstart') {
106 | quickstart();
107 |
108 | } else {
109 | console.error(cmdValue + ' : not recognized');
110 | }
111 |
112 | }
113 |
114 |
115 | // If one import this file, this is a module, otherwise a library
116 | if (require.main === module) {
117 | bin(process.argv);
118 | }
119 |
--------------------------------------------------------------------------------
/config.example:
--------------------------------------------------------------------------------
1 | {
2 | "user": "melvincarvalho",
3 | "twtfile": "/home/user/twtxt.txt",
4 | "limit_timeline": "20",
5 | "following": [
6 | {
7 | "user": "twtxt",
8 | "uri": "https://buckket.org/twtxt_news.txt"
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/lib/cli.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | tweet: tweet,
3 | follow: follow,
4 | unfollow: unfollow,
5 | following: following,
6 | quickstart: quickstart,
7 | timeline: timeline
8 | };
9 |
10 | // requires
11 | var fs = require('fs');
12 | var program = require('commander');
13 | var request = require('request');
14 | var debug = require('debug')('twtxt');
15 | var childProcess = require('child_process');
16 | var readline = require('readline');
17 | var moment = require('moment');
18 |
19 | var writeTweet = require("../lib/file").writeTweet;
20 | var writeConfig = require("../lib/file").writeConfig;
21 |
22 | var hook = require("../lib/helper").hook;
23 | var getUserHome = require("../lib/helper").getUserHome;
24 | var getUserName = require("../lib/helper").getUserName;
25 |
26 | var getConfigFile = require("../lib/config").getConfigFile;
27 | var getConfig = require("../lib/config").getConfig;
28 | var getTimelineFile = require("../lib/config").getTimelineFile;
29 |
30 | var parseTimeline = require("../lib/parser").parseTimeline;
31 | var displayPosts = require("../lib/parser").displayPosts;
32 | var serializeTweet = require("../lib/parser").serializeTweet;
33 |
34 | var error = debug('app:error');
35 |
36 | // defaults
37 | var default_limit_timeline = 20;
38 |
39 |
40 |
41 | /**
42 | * Append a new tweet to your twtxt file.
43 | * @return {[type]} [description]
44 | */
45 | function tweet(description) {
46 | // init
47 | var config = getConfig();
48 | var twtfile = getTimelineFile();
49 | output = serializeTweet(description);
50 |
51 | writeTweet(twtfile, output);
52 | if (config.post_tweet_hook) {
53 | hook(config.post_tweet_hook);
54 | }
55 | }
56 |
57 | /**
58 | * follow a user
59 | * @param {String} user the user to follow
60 | */
61 | function follow(user, uri) {
62 | var config = getConfig();
63 | config.following = config.following || [];
64 | config.following.push( {"user" : user, "uri": uri} );
65 | writeConfig(config);
66 | debug(config);
67 | console.log("✓ You’re now following "+user+".");
68 | }
69 |
70 | /**
71 | * list following
72 | */
73 | function following() {
74 | var config = getConfig();
75 | config.following = config.following || [];
76 | for (var i = 0; i < config.following.length; i++) {
77 | console.log('➤ ' + config.following[i].user + ' @ ' + config.following[i].uri);
78 | }
79 | }
80 |
81 | /**
82 | * unfollow a user
83 | * @param {String} user the user to follow
84 | */
85 | function unfollow(user) {
86 | var config = getConfig();
87 | config.following = config.following || [];
88 | for (var i = 0; i < config.following.length; i++) {
89 | if (config.following[i] && config.following[i].user === user ) {
90 | config.following.splice(i,1);
91 | }
92 | }
93 | writeConfig(config);
94 | debug(config);
95 | console.log("✓ You’ve unfollowed "+user+".");
96 | }
97 |
98 | /**
99 | * list following
100 | */
101 | function timeline() {
102 | var config = getConfig();
103 | var nick = config.user || 'you';
104 | var following = config.following || [];
105 | var fetched = 0;
106 | var sources = 1 + following.length;
107 | var posts = [];
108 |
109 | function callback(err, val) {
110 | fetched++;
111 | if (err) {
112 | console.error(err);
113 | } else {
114 | posts = posts.concat(val);
115 | if (fetched === sources) {
116 | displayPosts(posts);
117 | }
118 | }
119 | }
120 |
121 | // local
122 | var twtfile = getTimelineFile();
123 | parseTimeline(twtfile, nick, callback);
124 |
125 | // remote
126 | for (var i = 0; i < following.length; i++) {
127 |
128 | var uri = following[i].uri;
129 | parseTimeline(uri, following[i].user, callback);
130 |
131 | }
132 |
133 | }
134 |
135 | /**
136 | * quick start
137 | */
138 | function quickstart() {
139 |
140 | var rl = readline.createInterface({
141 | input: process.stdin,
142 | output: process.stdout
143 | });
144 |
145 | var help = "This wizard will generate a basic configuration file for twtxt with all mandatory options set. Have a look at the README.rst to get information about the other available options and their meaning.";
146 |
147 | console.log(help);
148 | var defaultNick = getUserName();
149 | var defaultPath = getTimelineFile();
150 | var defaultFollow = true;
151 |
152 | rl.question('➤ Please enter your desired nick: ( '+ defaultNick +' ) ', function(nick) {
153 | nick = nick || defaultNick;
154 |
155 | rl.question('➤ Please enter the desired location for your twtxt file: ( '+ defaultPath +' ) ', function(path) {
156 | path = path || defaultPath;
157 |
158 | rl.question('➤ Do you want to follow the twtxt news feed?: ( '+ (defaultFollow?'y':'n') + ' ) ', function(follow) {
159 | follow = follow || defaultFollow;
160 | debug('Thank you: ', nick);
161 | debug('Path: ', path);
162 | debug('Follow: ', follow);
163 |
164 | config = {};
165 | config.user = nick;
166 | config.twtfile = path;
167 | config.limit_timeline = "20";
168 | if (follow) {
169 | config.following = [ {"user" : "twtxt", "uri": "https://buckket.org/twtxt_news.txt" }];
170 | rl.close();
171 | debug(config);
172 | fs.writeFile(getConfigFile(), JSON.stringify(config));
173 | console.log('✓ Created config file at ' + getConfigFile());
174 | }
175 |
176 | });
177 |
178 | });
179 |
180 | });
181 |
182 | }
183 |
--------------------------------------------------------------------------------
/lib/config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | getConfigFile: getConfigFile,
3 | getConfig: getConfig,
4 | getTimelineFile: getTimelineFile
5 | };
6 |
7 | var debug = require('debug')('twtxt');
8 | var getUserHome = require("../lib/helper").getUserHome;
9 | var program = require('commander');
10 |
11 |
12 | /**
13 | * gets the default config file
14 | * @return {String} returns location of config file
15 | */
16 | function getConfigFile() {
17 | var defaultConfigFile = 'twtxt.json';
18 | var ret = getUserHome() + '/.config/' + defaultConfigFile;
19 | debug('default : ' + ret);
20 | return program.config || ret;
21 | }
22 |
23 | /**
24 | * gets a config
25 | * @return {String} A config
26 | */
27 | function getConfig() {
28 | var configFile = getConfigFile();
29 | debug('getting config from : ' + configFile);
30 | try {
31 | var config = require(configFile);
32 | debug('config : ');
33 | debug(config);
34 | return (config);
35 | } catch (e) {
36 | console.error(e);
37 | process.exit(-1);
38 | }
39 | }
40 |
41 | /**
42 | * returns location of timeline
43 | * @return {[String]} location of timeline
44 | */
45 | function getTimelineFile() {
46 | var config = getConfig();
47 |
48 | var HOME = getUserHome();
49 | var timelineFile = HOME + '/' + filename;
50 |
51 | var filename = config.twtfile || timelineFile;
52 |
53 | return filename;
54 | }
55 |
--------------------------------------------------------------------------------
/lib/file.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | writeConfig: writeConfig,
3 | writeTweet: writeTweet
4 | };
5 |
6 | var debug = require('debug')('twtxt');
7 | var fs = require('fs');
8 |
9 |
10 | var getUserHome = require("../lib/helper").getUserHome;
11 |
12 | var getConfigFile = require("../lib/config").getConfigFile;
13 | var configFile = require("../lib/config").configFile;
14 | var getConfig = require("../lib/config").getConfig;
15 | var parsePost = require("../lib/parser").parsePost;
16 | var parseTimeline = require("../lib/parser").parseTimeline;
17 |
18 | /**
19 | * write a config file
20 | * @param {Object} config the config
21 | */
22 | function writeConfig(config) {
23 | var configFile = getConfigFile();
24 | debug('writing : ');
25 | debug(config);
26 | fs.writeFile(configFile, JSON.stringify(config), function(err) {
27 | if (err) {
28 | console.error(err);
29 | }
30 | });
31 | }
32 |
33 | /**
34 | * writes a tweet
35 | * @param {String} twtfile file to write to
36 | * @param {[type]} output tweet
37 | */
38 | function writeTweet(twtfile, output) {
39 | var config = getConfig();
40 | parseTimeline(twtfile, config.user, function(err, posts) {
41 | str = '';
42 | posts = posts.sort(function(a,b){
43 | return new Date(a.time) > new Date(b.time);
44 | });
45 | posts = posts.concat(parsePost(output, config.user));
46 | var limit_timeline = config.limit_timeline || default_limit_timeline;
47 | if (posts.length > limit_timeline) {
48 | posts.splice(0, posts.length - limit_timeline);
49 | }
50 |
51 | for (var i = 0; i < posts.length; i++) {
52 | str += posts[i].time + '\t' + posts[i].description + '\n';
53 | }
54 |
55 | debug('writing ' + twtfile + ' with ' + str);
56 | fs.writeFile(twtfile, str, function (err) {
57 | if (err) {
58 | console.error(err);
59 | }
60 | });
61 | });
62 | }
63 |
--------------------------------------------------------------------------------
/lib/helper.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | hook: hook,
3 | getUserHome: getUserHome,
4 | getUserName: getUserName
5 | };
6 |
7 | var childProcess = require('child_process');
8 | var debug = require('debug')('twtxt');
9 |
10 | /**
11 | * hook
12 | * @param {String} hook run a hook
13 | */
14 | function hook(command) {
15 | var proc = childProcess.exec(command, function (error, stdout, stderr) {
16 | if (error) {
17 | console.error(error.stack);
18 | console.error('Error code: '+error.code);
19 | console.error('Signal received: '+error.signal);
20 | }
21 | debug('Child Process STDOUT: '+stdout);
22 | console.error('Child Process STDERR: '+stderr);
23 | });
24 |
25 | proc.on('exit', function (code) {
26 | debug('Child process exited with exit code '+code);
27 | });
28 | }
29 |
30 | /**
31 | * getUserHome
32 | * @return {String} home directory
33 | */
34 | function getUserHome() {
35 | return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
36 | }
37 |
38 | /**
39 | * getUserName
40 | * @return {String} user name
41 | */
42 | function getUserName() {
43 | return process.env.USER;
44 | }
45 |
--------------------------------------------------------------------------------
/lib/parser.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | serializeTweet: serializeTweet,
3 | parseTimeline: parseTimeline,
4 | parsePost: parsePost,
5 | displayPosts: displayPosts
6 | };
7 |
8 | var debug = require('debug')('twtxt');
9 | var getUserHome = require("../lib/helper").getUserHome;
10 | var fs = require('fs');
11 | var moment = require('moment');
12 | var request = require('request');
13 |
14 |
15 | /**
16 | * serializes a tweet
17 | * @param {String} description The desscription
18 | * @param {[type]} date Date
19 | * @return {String} formatted entry
20 | */
21 | function serializeTweet(description, date) {
22 | var now = date || new Date().toISOString();
23 | var output = now + "\t" + description + '\n';
24 | debug('blogging : ' + output);
25 | return output;
26 | }
27 |
28 | /**
29 | * parses a post
30 | * @param {String} post A post
31 | * @param {String} nick A nick
32 | * @return {Object} A time, description and nick
33 | */
34 | function parsePost(post, nick) {
35 | var vals = post.split('\t');
36 | return { "time" : vals[0], "description" : vals[1], "nick" : nick };
37 | }
38 |
39 | /**
40 | * displays the posts
41 | * @param {Array} posts the posts to display
42 | */
43 | function displayPosts(posts) {
44 | posts = posts.sort(function(a,b) {
45 | var datea = new Date(a.time).getTime();
46 | var dateb = new Date(b.time).getTime();
47 | return datea > dateb ? 1 : -1;
48 | });
49 | for (var i = 0; i < posts.length; i++) {
50 | console.log("➤ " + posts[i].nick + ' (' + moment(posts[i].time).fromNow() + ')');
51 | console.log(posts[i].description);
52 | }
53 | }
54 |
55 |
56 |
57 | /**
58 | * parseTimeline
59 | * @param {String} uri which uri
60 | * @param {String} nick which nick
61 | * @param {Function} callback The callback
62 | */
63 | function parseTimeline(uri, nick, callback) {
64 | if (uri && uri.indexOf('http') === 0) {
65 | request(uri, function (error, response, body) {
66 | if (!error && response.statusCode == 200) {
67 | var ret = [];
68 | var arr = body.split('\n');
69 | for (var i = 0; i < arr.length; i++) {
70 | if (arr[i]) {
71 | ret.push(parsePost(arr[i], nick));
72 | }
73 | }
74 | callback(null, (ret));
75 | } else {
76 | callback(error);
77 | }
78 | });
79 | } else {
80 | fs.readFile(uri, "utf-8", function(err, val) {
81 | if (err) {
82 | callback(err);
83 | } else {
84 | var ret = [];
85 | var arr = val.split('\n');
86 | for (var i = 0; i < arr.length; i++) {
87 | if (arr[i]) {
88 | ret.push(parsePost(arr[i], nick));
89 | }
90 | }
91 | callback(null, (ret));
92 | }
93 | });
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "twtxt",
3 | "version": "0.1.13",
4 | "description": "a decentralised, minimalist microblogging service for hackers",
5 | "main": "twtxt.js",
6 | "directories": {
7 | "test": "tests"
8 | },
9 | "scripts": {
10 | "test": "echo \"Error: no test specified\" && exit 1"
11 | },
12 | "repository": {
13 | "type": "git",
14 | "url": "git+https://github.com/webize/twtxt.git"
15 | },
16 | "keywords": [
17 | "twtxt",
18 | "solid"
19 | ],
20 | "author": "Melvin Carvalho ",
21 | "license": "MIT",
22 | "bugs": {
23 | "url": "https://github.com/webize/twtxt/issues"
24 | },
25 | "homepage": "https://github.com/webize/twtxt#readme",
26 | "dependencies": {
27 | "commander": "^2.9.0",
28 | "debug": "^2.2.0",
29 | "moment": "^2.11.2",
30 | "request": "^2.69.0"
31 | },
32 | "scripts": {
33 | "test": "mocha"
34 | },
35 | "bin": {
36 | "twtxt": "bin/twtxt.js"
37 | },
38 | "devDependencies": {
39 | "chai": "^3.5.0",
40 | "mocha": "^2.4.5"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/test/cli.js:
--------------------------------------------------------------------------------
1 |
2 |
3 | var cli = require("../lib/cli");
4 |
5 | /* test/my_test.js */
6 | var expect = require('chai').expect;
7 |
8 | describe('Cli Functions', function () {
9 |
10 | describe('tweet', function() {
11 | it('tweet is a function', function () {
12 | expect( (cli.tweet)).to.be.a('function');
13 | });
14 | });
15 |
16 | describe('follow', function() {
17 | it('follow is a function', function () {
18 | expect( (cli.follow)).to.be.a('function');
19 | });
20 | });
21 |
22 | describe('unfollow', function() {
23 | it('unfollow is a function', function () {
24 | expect( (cli.unfollow)).to.be.a('function');
25 | });
26 | });
27 |
28 | describe('following', function() {
29 | it('following is a function', function () {
30 | expect( (cli.following)).to.be.a('function');
31 | });
32 | });
33 |
34 | describe('quickstart', function() {
35 | it('quickstart is a function', function () {
36 | expect( (cli.quickstart)).to.be.a('function');
37 | });
38 | });
39 |
40 | describe('timeline', function() {
41 | it('timeline is a function', function () {
42 | expect( (cli.timeline)).to.be.a('function');
43 | });
44 | });
45 |
46 |
47 | });
48 |
--------------------------------------------------------------------------------
/test/config.js:
--------------------------------------------------------------------------------
1 | var config = require("../lib/config");
2 |
3 | /* test/my_test.js */
4 | var expect = require('chai').expect;
5 |
6 | describe('Config Functions', function () {
7 |
8 | describe('getConfigFile', function() {
9 | it('getConfigFile is a function', function () {
10 | expect( (config.getConfigFile)).to.be.a('function');
11 | });
12 | });
13 |
14 | describe('getConfig', function() {
15 | it('getConfig is a function', function () {
16 | expect( (config.getConfig)).to.be.a('function');
17 | });
18 | });
19 |
20 | describe('getTimelineFile', function() {
21 | it('getTimelineFile is a function', function () {
22 | expect( (config.getTimelineFile)).to.be.a('function');
23 | });
24 | });
25 |
26 | });
27 |
--------------------------------------------------------------------------------
/test/file.js:
--------------------------------------------------------------------------------
1 | var file = require("../lib/file");
2 |
3 | /* test/my_test.js */
4 | var expect = require('chai').expect;
5 |
6 | describe('File Functions', function () {
7 |
8 | describe('writeConfig', function() {
9 | it('writeConfig is a function', function () {
10 | expect( (file.writeConfig)).to.be.a('function');
11 | });
12 | });
13 |
14 | describe('writeTweet', function() {
15 | it('writeTweet is a function', function () {
16 | expect( (file.writeTweet)).to.be.a('function');
17 | });
18 | });
19 |
20 | });
21 |
--------------------------------------------------------------------------------
/test/helper.js:
--------------------------------------------------------------------------------
1 | var helper = require("../lib/helper");
2 |
3 | /* test/my_test.js */
4 | var expect = require('chai').expect;
5 |
6 | describe('Helper Functions', function () {
7 |
8 | describe('hook', function() {
9 | it('hook is a function', function () {
10 | expect( (helper.hook)).to.be.a('function');
11 | });
12 | });
13 |
14 | describe('getUserHome', function() {
15 | it('getUserHome is a function', function () {
16 | expect( (helper.getUserHome)).to.be.a('function');
17 | });
18 | });
19 |
20 | describe('getUserName', function() {
21 | it('getUserName is a function', function () {
22 | expect( (helper.getUserName)).to.be.a('function');
23 | });
24 | });
25 |
26 | });
27 |
--------------------------------------------------------------------------------
/test/parser.js:
--------------------------------------------------------------------------------
1 | var parser = require("../lib/parser");
2 |
3 | /* test/my_test.js */
4 | var expect = require('chai').expect;
5 |
6 | describe('Parser Functions', function () {
7 |
8 | describe('serializeTweet', function() {
9 | it('serializeTweet is a function', function () {
10 | expect( (parser.serializeTweet)).to.be.a('function');
11 | });
12 | it('serializeTweet gives the right output', function () {
13 | expect(parser.serializeTweet('test', '2016-02-07T19:55:58.254Z')).to.equal('2016-02-07T19:55:58.254Z\ttest\n');
14 | });
15 | });
16 |
17 | describe('parseTimeline', function() {
18 | it('parseTimeline is a function', function () {
19 | expect( (parser.parseTimeline)).to.be.a('function');
20 | });
21 | });
22 |
23 | describe('parsePost', function() {
24 | it('parsePost is a function', function () {
25 | expect( (parser.parsePost)).to.be.a('function');
26 | });
27 | });
28 |
29 | describe('displayPosts', function() {
30 | it('displayPosts is a function', function () {
31 | expect( (parser.displayPosts)).to.be.a('function');
32 | });
33 | });
34 |
35 | });
36 |
--------------------------------------------------------------------------------
/twtxt.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // requires
4 | var fs = require('fs');
5 | var program = require('commander');
6 | var request = require('request');
7 | var debug = require('debug')('twtxt');
8 | var childProcess = require('child_process');
9 | var readline = require('readline');
10 | var moment = require('moment');
11 |
12 | var error = debug('app:error');
13 |
14 | // defaults
15 | var default_limit_timeline = 20;
16 |
17 |
18 | // helper functions
19 |
20 | /**
21 | * hook
22 | * @param {String} hook run a hook
23 | */
24 | function hook(command) {
25 | var proc = childProcess.exec(command, function (error, stdout, stderr) {
26 | if (error) {
27 | console.error(error.stack);
28 | console.error('Error code: '+error.code);
29 | console.error('Signal received: '+error.signal);
30 | }
31 | debug('Child Process STDOUT: '+stdout);
32 | console.error('Child Process STDERR: '+stderr);
33 | });
34 |
35 | proc.on('exit', function (code) {
36 | debug('Child process exited with exit code '+code);
37 | });
38 | }
39 |
40 |
41 |
42 | // config functions
43 |
44 | /**
45 | * getUserHome
46 | * @return {String} home directory
47 | */
48 | function getUserHome() {
49 | return process.env[(process.platform == 'win32') ? 'USERPROFILE' : 'HOME'];
50 | }
51 |
52 | /**
53 | * getUserName
54 | * @return {String} user name
55 | */
56 | function getUserName() {
57 | return process.env.USER;
58 | }
59 |
60 | /**
61 | * gets the default config file
62 | * @return {String} returns location of config file
63 | */
64 | function getConfigFile() {
65 | var defaultConfigFile = 'twtxt.json';
66 | var ret = getUserHome() + '/.config/' + defaultConfigFile;
67 | debug('default : ' + ret);
68 | return program.config || ret;
69 | }
70 |
71 | /**
72 | * gets a config
73 | * @return {String} A config
74 | */
75 | function getConfig() {
76 | var configFile = getConfigFile();
77 | debug('getting config from : ' + configFile);
78 | try {
79 | var config = require(configFile);
80 | debug('config : ');
81 | debug(config);
82 | return (config);
83 | } catch (e) {
84 | console.error(e);
85 | process.exit(-1);
86 | }
87 | }
88 |
89 | /**
90 | * write a config file
91 | * @param {Object} config the config
92 | */
93 | function writeConfig(config) {
94 | var configFile = getConfigFile();
95 | debug('writing : ');
96 | debug(config);
97 | fs.writeFile(configFile, JSON.stringify(config), function(err) {
98 | if (err) {
99 | console.error(err);
100 | }
101 | });
102 | }
103 |
104 | // twtxt functions
105 |
106 | /**
107 | * returns location of timeline
108 | * @return {[String]} location of timeline
109 | */
110 | function getTimelineFile() {
111 | var config = getConfig();
112 |
113 | var HOME = getUserHome();
114 | var timelineFile = HOME + '/' + filename;
115 |
116 | var filename = config.twtfile || timelineFile;
117 |
118 | return filename;
119 | }
120 |
121 | /**
122 | * serializes a tweet
123 | * @param {String} description The desscription
124 | * @param {[type]} date Date
125 | * @return {String} formatted entry
126 | */
127 | function serializeTweet(description, date) {
128 | var now = date || new Date().toISOString();
129 | var output = now + "\t" + description + '\n';
130 | debug('blogging : ' + output);
131 | return output;
132 | }
133 |
134 | /**
135 | * parseTimeline
136 | * @param {String} uri which uri
137 | * @param {String} nick which nick
138 | * @param {Function} callback The callback
139 | */
140 | function parseTimeline(uri, nick, callback) {
141 | if (uri && uri.indexOf('http') === 0) {
142 | request(uri, function (error, response, body) {
143 | if (!error && response.statusCode == 200) {
144 | var ret = [];
145 | var arr = body.split('\n');
146 | for (var i = 0; i < arr.length; i++) {
147 | if (arr[i]) {
148 | ret.push(parsePost(arr[i], nick));
149 | }
150 | }
151 | callback(null, (ret));
152 | } else {
153 | callback(error);
154 | }
155 | });
156 | } else {
157 | fs.readFile(uri, "utf-8", function(err, val) {
158 | if (err) {
159 | callback(err);
160 | } else {
161 | var ret = [];
162 | var arr = val.split('\n');
163 | for (var i = 0; i < arr.length; i++) {
164 | if (arr[i]) {
165 | ret.push(parsePost(arr[i], nick));
166 | }
167 | }
168 | callback(null, (ret));
169 | }
170 | });
171 | }
172 | }
173 |
174 | /**
175 | * parses a post
176 | * @param {String} post A post
177 | * @param {String} nick A nick
178 | * @return {Object} A time, description and nick
179 | */
180 | function parsePost(post, nick) {
181 | var vals = post.split('\t');
182 | return { "time" : vals[0], "description" : vals[1], "nick" : nick };
183 | }
184 |
185 | /**
186 | * writes a tweet
187 | * @param {String} twtfile file to write to
188 | * @param {[type]} output tweet
189 | */
190 | function writeTweet(twtfile, output) {
191 | var config = getConfig();
192 | parseTimeline(twtfile, config.user, function(err, posts) {
193 | str = '';
194 | posts = posts.sort(function(a,b){
195 | return new Date(a.time) > new Date(b.time);
196 | });
197 | posts = posts.concat(parsePost(output, config.user));
198 | var limit_timeline = config.limit_timeline || default_limit_timeline;
199 | if (posts.length > limit_timeline) {
200 | posts.splice(0, posts.length - limit_timeline);
201 | }
202 |
203 | for (var i = 0; i < posts.length; i++) {
204 | str += posts[i].time + '\t' + posts[i].description + '\n';
205 | }
206 |
207 | debug('writing ' + twtfile + ' with ' + str);
208 | fs.writeFile(twtfile, str, function (err) {
209 | if (err) {
210 | console.error(err);
211 | }
212 | });
213 | });
214 | }
215 |
216 | /**
217 | * Append a new tweet to your twtxt file.
218 | * @return {[type]} [description]
219 | */
220 | function tweet(description) {
221 | // init
222 | var config = getConfig();
223 | var twtfile = getTimelineFile();
224 | output = serializeTweet(description);
225 |
226 | writeTweet(twtfile, output);
227 | if (config.post_tweet_hook) {
228 | hook(config.post_tweet_hook);
229 | }
230 | }
231 |
232 | /**
233 | * displays the posts
234 | * @param {Array} posts the posts to display
235 | */
236 | function displayPosts(posts) {
237 | posts = posts.sort(function(a,b){
238 | return new Date(a.time) > new Date(b.time);
239 | });
240 | for (var i = 0; i < posts.length; i++) {
241 | console.log("➤ " + posts[i].nick + ' (' + moment(posts[i].time).fromNow() + ')');
242 | console.log(posts[i].description);
243 | }
244 | }
245 |
246 | /**
247 | * follow a user
248 | * @param {String} user the user to follow
249 | */
250 | function follow(user, uri) {
251 | var config = getConfig();
252 | config.following = config.following || [];
253 | config.following.push( {"user" : user, "uri": uri} );
254 | writeConfig(config);
255 | debug(config);
256 | console.log("✓ You’re now following "+user+".");
257 | }
258 |
259 | /**
260 | * list following
261 | */
262 | function following() {
263 | var config = getConfig();
264 | config.following = config.following || [];
265 | for (var i = 0; i < config.following.length; i++) {
266 | console.log('➤ ' + config.following[i].user + ' @ ' + config.following[i].uri);
267 | }
268 | }
269 |
270 | /**
271 | * unfollow a user
272 | * @param {String} user the user to follow
273 | */
274 | function unfollow(user) {
275 | var config = getConfig();
276 | config.following = config.following || [];
277 | for (var i = 0; i < config.following.length; i++) {
278 | if (config.following[i] && config.following[i].user === user ) {
279 | config.following.splice(i,1);
280 | }
281 | }
282 | writeConfig(config);
283 | debug(config);
284 | console.log("✓ You’ve unfollowed "+user+".");
285 | }
286 |
287 | /**
288 | * list following
289 | */
290 | function timeline() {
291 | var config = getConfig();
292 | var nick = config.user || 'you';
293 | var following = config.following || [];
294 | var fetched = 0;
295 | var sources = 1 + following.length;
296 | var posts = [];
297 |
298 | function callback(err, val) {
299 | fetched++;
300 | if (err) {
301 | console.error(err);
302 | } else {
303 | posts = posts.concat(val);
304 | if (fetched === sources) {
305 | displayPosts(posts);
306 | }
307 | }
308 | }
309 |
310 | // local
311 | var twtfile = getTimelineFile();
312 | parseTimeline(twtfile, nick, callback);
313 |
314 | // remote
315 | for (var i = 0; i < following.length; i++) {
316 |
317 | var uri = following[i].uri;
318 | parseTimeline(uri, following[i].user, callback);
319 |
320 | }
321 |
322 | }
323 |
324 | /**
325 | * quick start
326 | */
327 | function quickstart() {
328 |
329 | var rl = readline.createInterface({
330 | input: process.stdin,
331 | output: process.stdout
332 | });
333 |
334 | var help = "This wizard will generate a basic configuration file for twtxt with all mandatory options set. Have a look at the README.rst to get information about the other available options and their meaning.";
335 |
336 | console.log(help);
337 | var defaultNick = getUserName();
338 | var defaultPath = getTimelineFile();
339 | var defaultFollow = true;
340 |
341 | rl.question('➤ Please enter your desired nick: ( '+ defaultNick +' ) ', function(nick) {
342 | nick = nick || defaultNick;
343 |
344 | rl.question('➤ Please enter the desired location for your twtxt file: ( '+ defaultPath +' ) ', function(path) {
345 | path = path || defaultPath;
346 |
347 | rl.question('➤ Do you want to follow the twtxt news feed?: ( '+ (defaultFollow?'y':'n') + ' ) ', function(follow) {
348 | follow = follow || defaultFollow;
349 | debug('Thank you: ', nick);
350 | debug('Path: ', path);
351 | debug('Follow: ', follow);
352 |
353 | config = {};
354 | config.user = nick;
355 | config.twtfile = path;
356 | config.limit_timeline = "20";
357 | if (follow) {
358 | config.following = [ {"user" : "twtxt", "uri": "https://buckket.org/twtxt_news.txt" }];
359 | rl.close();
360 | debug(config);
361 | fs.writeFile(getConfigFile(), JSON.stringify(config));
362 | console.log('✓ Created config file at ' + getConfigFile());
363 | }
364 |
365 | });
366 |
367 | });
368 |
369 | });
370 |
371 | }
372 |
373 | // cli functions
374 | /**
375 | * run as bin
376 | */
377 | function bin() {
378 | var MAXCHARS = 140;
379 | var user;
380 | var uri;
381 |
382 | program
383 | .arguments(' [arg] [uri]')
384 | .option('-c, --config ', 'Specify a custom config file location.')
385 | .option('-v, --verbose', 'Enable verbose output for deubgging purposes')
386 | .option('--version', 'Shows the version and exit.')
387 | .action(function (cmd, arg, uri) {
388 | cmdValue = cmd;
389 | argValue = arg;
390 | uriValue = uri;
391 | });
392 |
393 | program.parse(process.argv);
394 |
395 | if (typeof cmdValue === 'undefined') {
396 | console.error('no command given!');
397 | process.exit(1);
398 | }
399 | if (program.verbose) {
400 | process.env.DEBUG = 'twtxt';
401 | debug = console.log;
402 | }
403 | debug('command:', cmdValue);
404 | debug('arg: ', argValue || "no arg given");
405 | debug('uri: ', uriValue || "no uri given");
406 | debug('verbose: ' + program.verbose);
407 |
408 | if (cmdValue === 'tweet') {
409 | var description = argValue;
410 |
411 | if (!description) {
412 | console.error('Usage: twtxt tweet ');
413 | process.exit(-1);
414 | }
415 |
416 | if (description && description.length > MAXCHARS) {
417 | console.error('Maximum tweet length : ' + MAXCHARS);
418 | process.exit(-1);
419 | }
420 |
421 | tweet(description);
422 | } else if (cmdValue === 'follow') {
423 | user = argValue;
424 | uri = uriValue;
425 |
426 | if (!user) {
427 | console.error('Usage: twtxt follow ');
428 | process.exit(-1);
429 | }
430 |
431 | if (!uri) {
432 | console.error('Usage: twtxt follow ');
433 | process.exit(-1);
434 | }
435 |
436 | follow(user, uri);
437 |
438 | } else if (cmdValue === 'unfollow') {
439 | user = argValue;
440 |
441 | if (!user) {
442 | console.error('Usage: twtxt unfollow ');
443 | process.exit(-1);
444 | }
445 |
446 | unfollow(user);
447 |
448 | } else if (cmdValue === 'following') {
449 | following();
450 |
451 | } else if (cmdValue === 'timeline') {
452 | timeline();
453 |
454 | } else if (cmdValue === 'quickstart') {
455 | quickstart();
456 |
457 | } else {
458 | console.error(cmdValue + ' : not recognized');
459 | }
460 |
461 | }
462 |
463 |
464 | // If one import this file, this is a module, otherwise a library
465 | if (require.main === module) {
466 | bin(process.argv);
467 | }
468 |
469 | module.exports = {
470 | tweet: tweet,
471 | follow: follow,
472 | unfollow: unfollow,
473 | following: following,
474 | timeline: timeline
475 | };
476 |
--------------------------------------------------------------------------------