├── .gitignore ├── Procfile ├── README.md ├── avatar.png ├── bg.png ├── package.json ├── src ├── GridHelper.js ├── MinesweeperGame.js ├── createBoard.js ├── delay.js ├── gameStateToString.js ├── gridUtils.js ├── index.js ├── parseCommands.js ├── range.js ├── shuffle.js ├── throttle.js ├── toFullWidthString.js └── twit.js └── twitSettings.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config.sh 3 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: npm start -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [MineTweeter](http://twitter.com/minetweeter_) 2 | 3 | A twitter bot that generates MineSweeper games and accepts commands via @mentions. 4 | 5 | ## Key 6 | 7 | ``` 8 | ABCDEFGHI 9 | .........J 10 | 11..111..K 11 | =2..1^1..L 12 | =2..111..M 13 | =1111....N 14 | ===^31...O 15 | ====^1...P 16 | ====22232Q 17 | ====^1^^^R 18 | 19 | . REVEALED BLANK/EMPTY SPACE 20 | = NON-REVEALED SPACE 21 | ^ FLAGGED SPACE 22 | ``` 23 | 24 | ## Command Syntax 25 | 26 | ``` 27 | 28 | 29 | Available actions: 30 | - click 31 | - flag 32 | - unflag 33 | 34 | Example actions: 35 | click A J 36 | flag E K 37 | ``` 38 | 39 | ## Why would you make this? 40 | 41 | *Stares off into the distance, smiling manically.* 42 | 43 | Huh? 44 | 45 | ## Game Logic 46 | 47 | If you're looking for some example code for generating, rendering, and evaluating 48 | the state of minesweeper boards, check out these files: 49 | 50 | - src/createBoard.js 51 | - src/gameStateToString.js 52 | - src/MinesweeperGame.js 53 | 54 | ---- 55 | 56 | [![Analytics](https://ga-beacon.appspot.com/UA-33247419-2/minetweeter/README.md)](https://github.com/igrigorik/ga-beacon) 57 | -------------------------------------------------------------------------------- /avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namuol/minetweeter/2877a32bde7c02c7d31a77e049acae1a7909c180/avatar.png -------------------------------------------------------------------------------- /bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/namuol/minetweeter/2877a32bde7c02c7d31a77e049acae1a7909c180/bg.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "minetweeter", 3 | "version": "0.0.2", 4 | "description": "Minesweeper Twitter Bot", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "babel-node --stage 0 src", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "MIT", 12 | "dependencies": { 13 | "babel": "^5.4.7", 14 | "immutable": "^3.7.3", 15 | "twit": "^1.1.20" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/GridHelper.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import range from './range'; 3 | 4 | export default function GridHelper (params) { 5 | let { 6 | width, 7 | height, 8 | } = params; 9 | 10 | function xy (idx) { 11 | let x = idx % width; 12 | let y = Math.floor(idx / width); 13 | return [x, y]; 14 | } 15 | 16 | function neighbors4Indices (x, y) { 17 | return Immutable.List([ 18 | index(x, y-1), // N 19 | index(x, y+1), // S 20 | index(x+1, y), // E 21 | index(x-1, y), // W 22 | ]); 23 | } 24 | 25 | function neighbors4 (list, x, y) { 26 | return neighbors4Indices(x, y).map((idx) => { 27 | return list.get(idx); 28 | }); 29 | } 30 | 31 | function neighbors8Indices (x, y) { 32 | return Immutable.List([ 33 | index(x, y-1), // N 34 | index(x+1, y-1), // NE 35 | index(x-1, y-1), // NW 36 | index(x, y+1), // S 37 | index(x+1, y+1), // SE 38 | index(x-1, y+1), // SW 39 | index(x+1, y), // E 40 | index(x-1, y), // W 41 | ]); 42 | } 43 | 44 | function neighbors8 (list, x, y) { 45 | return neighbors8Indices(x, y).map((idx) => { 46 | return list.get(idx); 47 | }); 48 | } 49 | 50 | function index (x, y) { 51 | if ( 52 | x < 0 || 53 | y < 0 || 54 | x >= width || 55 | y >= height 56 | ) { 57 | return undefined; 58 | } 59 | 60 | return x + (y * width); 61 | } 62 | 63 | function flood (params) { 64 | let { 65 | data, 66 | predicate, 67 | mask = range(0, width*height).map(() => {return false;}), 68 | } = params; 69 | 70 | let processed = range(0, width*height).map(() => {return false;}); 71 | 72 | let queue = Immutable.List(); 73 | 74 | queue = queue.push([params.x, params.y]); 75 | let count = 0; 76 | while (queue.size > 0) { 77 | count += 1; 78 | 79 | let [x,y] = queue.first(); 80 | let idx = index(x,y); 81 | 82 | queue = queue.shift(); 83 | 84 | if (processed.get(idx)) { 85 | continue; 86 | } 87 | 88 | processed = processed.set(idx, true); 89 | 90 | if (!predicate({ 91 | data: data, 92 | x: x, y: y, 93 | value: data.get(idx), 94 | })) { 95 | continue; 96 | } 97 | 98 | mask = mask.set(idx, true); 99 | 100 | neighbors8Indices(x, y).filter((idx) => { 101 | return !processed.get(idx); 102 | }).forEach((idx) => { 103 | queue = queue.push(xy(idx)); 104 | }); 105 | } 106 | 107 | return mask; 108 | } 109 | 110 | return { 111 | flood: flood, 112 | index: index, 113 | xy: xy, 114 | neighbors4Indices: neighbors4Indices, 115 | neighbors8Indices: neighbors8Indices, 116 | neighbors4: neighbors4, 117 | neighbors8: neighbors8, 118 | }; 119 | }; 120 | -------------------------------------------------------------------------------- /src/MinesweeperGame.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | import GridHelper from './GridHelper'; 4 | import createBoard from './createBoard'; 5 | import range from './range'; 6 | 7 | export default function MinesweeperGame (params) { 8 | let { 9 | width, 10 | height, 11 | mineCount, 12 | } = params; 13 | 14 | let emptyBoard = createBoard({ 15 | width: width, 16 | height: height, 17 | mineCount: 0, 18 | }); 19 | 20 | let { 21 | xy, 22 | index, 23 | } = GridHelper({ 24 | width: width, 25 | height: height, 26 | }); 27 | 28 | let board = emptyBoard; 29 | 30 | let clicks = range(0, width*height).map(() => { 31 | return false; 32 | }); 33 | 34 | function click (x, y) { 35 | if (board == emptyBoard) { 36 | board = createBoard({ 37 | width: width, 38 | height: height, 39 | mineCount: mineCount, 40 | startX: x, 41 | startY: y, 42 | }); 43 | } 44 | 45 | let idx = index(x, y); 46 | flags = flags.set(idx, false); 47 | clicks = clicks.set(idx, true); 48 | return getGame(); 49 | } 50 | 51 | let flags = range(0, width*height).map(() => { 52 | return false; 53 | }); 54 | 55 | function flag (x, y) { 56 | flags = flags.set(index(x, y), true); 57 | return getGame(); 58 | } 59 | 60 | function unflag (x, y) { 61 | flags = flags.set(index(x, y), false); 62 | return getGame(); 63 | } 64 | 65 | function getState() { 66 | let isNew = board.get('mines').filter((m) => { 67 | return m; 68 | }).size === 0; 69 | 70 | return Immutable.Map({ 71 | board: board, 72 | clicks: clicks, 73 | flags: flags, 74 | new: isNew, 75 | lost: clicks.some((p, idx) => { 76 | return !isNew && p && board.get('mines').get(idx); 77 | }), 78 | won: flags.every((f, idx) => { 79 | return !isNew && f == board.get('mines').get(idx); 80 | }), 81 | }); 82 | } 83 | 84 | function getGame() { 85 | return { 86 | width: width, 87 | height: height, 88 | click: click, 89 | flag: flag, 90 | unflag: unflag, 91 | state: getState(), 92 | }; 93 | } 94 | 95 | return getGame(); 96 | } -------------------------------------------------------------------------------- /src/createBoard.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | import range from './range'; 4 | import shuffle from './shuffle'; 5 | // let shuffle = (v) => { return v; }; 6 | 7 | import GridHelper from './GridHelper'; 8 | 9 | export default function createBoard (params) { 10 | let { 11 | width, 12 | height, 13 | mineCount, 14 | startX, 15 | startY, 16 | } = params; 17 | 18 | let grid = GridHelper({ 19 | width: width, 20 | height: height, 21 | }); 22 | 23 | 24 | let mines = shuffle(range(0, width*height).map((val, index) => { 25 | if (index < mineCount) { 26 | return true; 27 | } 28 | return false; 29 | })); 30 | 31 | if ((typeof startX === 'number') && (typeof startY === 'number')) { 32 | let indicesToClear = grid.neighbors8Indices(startX, startY).push(grid.index(startX, startY)); 33 | 34 | let clearedCount = 0; 35 | mines = indicesToClear.reduce((result, idx) => { 36 | if (result.get(idx) === true) { 37 | // Swap with the first random safe spot 38 | clearedCount += 1; 39 | result = result.set(idx, false); 40 | } 41 | return result; 42 | }, mines); 43 | 44 | mines = shuffle(range(0,width*height).filter((idx) => { 45 | return mines.get(idx) === false && indicesToClear.indexOf(idx) < 0; 46 | })).take(clearedCount).reduce((result, idx) => { 47 | return result.set(idx, true); 48 | }, mines); 49 | } 50 | 51 | return Immutable.fromJS({ 52 | width: width, 53 | height: height, 54 | mines: mines, 55 | }); 56 | }; -------------------------------------------------------------------------------- /src/delay.js: -------------------------------------------------------------------------------- 1 | export default async function delay (ms) { 2 | return new Promise(function (resolve) { 3 | setTimeout(resolve, ms); 4 | }); 5 | } -------------------------------------------------------------------------------- /src/gameStateToString.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | import GridHelper from './GridHelper'; 3 | import range from './range'; 4 | 5 | let chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 6 | 7 | function charify (offset=0) { 8 | return (num) => { 9 | return chars[num+offset]; 10 | } 11 | } 12 | 13 | export default function gameStateToString (state) { 14 | let board = state.get('board'); 15 | 16 | let { 17 | index, 18 | xy, 19 | flood, 20 | neighbors8, 21 | neighbors4, 22 | neighbors8Indices, 23 | neighbors4Indices, 24 | } = GridHelper({ 25 | width: board.get('width'), 26 | height: board.get('height'), 27 | }); 28 | 29 | let width = board.get('width'); 30 | let height = board.get('height'); 31 | 32 | let clicks = state.get('clicks'); 33 | let flags = state.get('flags'); 34 | 35 | let lost = state.get('lost'); 36 | 37 | let mines = board.get('mines'); 38 | 39 | let hidden = mines.map((isMine, idx) => { 40 | if (isMine) { 41 | return '@'; 42 | } 43 | 44 | let count = neighbors8(mines, ...xy(idx)).count((v) => { return v === true; }); 45 | 46 | if (count === 0) { 47 | return '.'; 48 | } else { 49 | return count; 50 | } 51 | }); 52 | 53 | let revealMask = clicks.reduce((mask, click, idx) => { 54 | if (!click) { 55 | return mask; 56 | } 57 | 58 | let [clickX, clickY] = xy(idx); 59 | return flood({ 60 | data: hidden, 61 | mask: mask, 62 | x: clickX, y: clickY, 63 | predicate: (params) => { 64 | let { 65 | x, y, 66 | value, 67 | } = params; 68 | 69 | return value === '.'; 70 | }, 71 | }); 72 | }, clicks).map((val, idx) => { 73 | return val || clicks.get(idx); 74 | }); 75 | 76 | revealMask = revealMask.reduce((result, thisSpotIsRevealed, idx) => { 77 | let reveal; 78 | 79 | if (thisSpotIsRevealed) { 80 | reveal = true; 81 | } else { 82 | reveal = neighbors8Indices(...xy(idx)).some((idx) => { 83 | return revealMask.get(idx) && hidden.get(idx) === '.'; 84 | }); 85 | } 86 | 87 | return result.set(idx, reveal); 88 | }, revealMask); 89 | 90 | let revealed = hidden.map((char, idx) => { 91 | if (revealMask.get(idx)) { 92 | if (mines.get(idx)) { 93 | return 'X'; 94 | } 95 | return char; 96 | } else { 97 | if (flags.get(idx)) { 98 | return '^'; 99 | } else if (lost && mines.get(idx)) { 100 | return '@'; 101 | } else { 102 | return '='; 103 | } 104 | } 105 | }); 106 | 107 | let horizLabel = range(0,width).map(charify(10)).join(''); 108 | 109 | function labelify (list) { 110 | return horizLabel + '\n' + list.reduce((result, char, idx) => { 111 | let [x, y] = xy(idx); 112 | 113 | result += char; 114 | 115 | if (x === board.get('width') - 1) { 116 | result += `${charify(10+width)(y)}\n`; 117 | } 118 | 119 | return result; 120 | }, ''); 121 | }; 122 | 123 | return labelify(revealed) 124 | // + '\n\n' + labelify(hidden.map((ch) => { 125 | // if (ch === '@' || ch === '.') { 126 | // return ch; 127 | // } 128 | 129 | // return ch; 130 | // })); 131 | }; -------------------------------------------------------------------------------- /src/gridUtils.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | export default function gridHelper (params) { 4 | let { 5 | width, 6 | height, 7 | } = params; 8 | 9 | function indexToXY (params) { 10 | let { 11 | width, 12 | index, 13 | } = params; 14 | 15 | let x = index % width; 16 | let y = Math.floor(index / width); 17 | return [x, y]; 18 | } 19 | 20 | function xyToIndex (params) { 21 | let { 22 | width, 23 | x, 24 | y, 25 | } = params; 26 | 27 | return x + (y * width); 28 | } 29 | 30 | return { 31 | indexToXY: indexToXY, 32 | xyToIndex: xyToIndex, 33 | }; 34 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import twit from './twit'; 2 | 3 | import 'babel/polyfill'; 4 | 5 | import MinesweeperGame from './MinesweeperGame'; 6 | import toFullWidthString from './toFullWidthString'; 7 | import gameStateToString from './gameStateToString'; 8 | import range from './range'; 9 | import throttle from './throttle'; 10 | import shuffle from './shuffle'; 11 | import parseCommands from './parseCommands'; 12 | 13 | import Immutable from 'immutable'; 14 | 15 | let mineCount = 10; 16 | 17 | function newGame () { 18 | return MinesweeperGame({ 19 | width: 9, 20 | height: 9, 21 | mineCount: mineCount, 22 | }); 23 | } 24 | 25 | let game = newGame(); 26 | 27 | let mentions = twit.stream('statuses/filter', {track: '@minetweeter_'}); 28 | 29 | let uniqueMessages = shuffle(Immutable.fromJS([ 30 | 'Begin!', 31 | 'New Game!', 32 | 'Okay!', 33 | 'Yup.', 34 | 'Hmm.', 35 | 'Welp.', 36 | 'Heh.', 37 | 'Yeah.', 38 | 'BAM!', 39 | ])); 40 | 41 | function getUniqueMessage () { 42 | let msg = uniqueMessages.first(); 43 | uniqueMessages = uniqueMessages.shift().push(msg); 44 | return msg; 45 | } 46 | 47 | async function tweetGameBoard (params) { 48 | let { 49 | gameState, 50 | user, 51 | } = params; 52 | 53 | let board = toFullWidthString(gameStateToString(gameState)); 54 | 55 | let status = board; 56 | 57 | if (gameState.get('new')) { 58 | status = `${board}\n\n${getUniqueMessage()} ${mineCount} mines remain!`; 59 | } else if (gameState.get('lost')) { 60 | status = `${board}\n\nKaboom, @${user}!`; 61 | } else if (gameState.get('won')) { 62 | status = `${board}\n\nBravo, @${user}!`; 63 | } 64 | 65 | await twit.post('statuses/update', { 66 | status: status, 67 | }); 68 | 69 | return status; 70 | } 71 | 72 | async function handleCommandString (params) { 73 | let { 74 | game, 75 | commandString, 76 | user, 77 | } = params; 78 | 79 | let commands = parseCommands({ 80 | status: commandString, 81 | width: game.width, 82 | height: game.height, 83 | }); 84 | 85 | if (commands.length === 0) { 86 | // TODO: Tweet at the user "Sorry, I didn't understand that. These are the commands:..." 87 | twit.post('statuses/update', { 88 | status: `@${user}\n\n${commandFailedMessage}`, 89 | }); 90 | } 91 | 92 | commands.forEach((command) => { 93 | game = game[command.type || 'click'](command.x, command.y); 94 | }); 95 | 96 | return game; 97 | } 98 | 99 | let commandFailedMessage = 100 | `Action format: 101 | [click|flag|unflag] x y 102 | 103 | You can tweet many actions at once. 104 | 105 | Key: 106 | = UNKNOWN 107 | . BLANK 108 | ^ FLAG`; 109 | 110 | let onMention = throttle(100, async function (tweet) { 111 | console.log(`Got tweet from ${tweet.user.screen_name}:`, tweet.text); 112 | 113 | let previousState = game.state; 114 | 115 | game = await handleCommandString({ 116 | game: game, 117 | commandString: tweet.text, 118 | user: tweet.user.screen_name, 119 | }); 120 | 121 | if (previousState === game.state) { 122 | // Nothing changed; no need to tweet... or maybe we could tweet a message to the user 123 | // that nothing changed? 124 | return; 125 | } 126 | 127 | try { 128 | let status = await tweetGameBoard({ 129 | gameState: game.state, 130 | user: tweet.user.screen_name, 131 | }); 132 | 133 | console.log(status); 134 | } catch (e) { 135 | console.error('Failed to tweet game board', e); 136 | } 137 | 138 | if (game.state.get('lost') || game.state.get('won')) { 139 | game = newGame(); 140 | onMention.clearQueue(); 141 | try { 142 | let status = await tweetGameBoard({ 143 | gameState: game.state, 144 | }); 145 | 146 | console.log(status); 147 | } catch (e) { 148 | console.error('Failed to tweet game board', e); 149 | } 150 | } 151 | }); 152 | 153 | mentions.on('tweet', onMention); 154 | 155 | process.stdin.on('data', (commandText) => { 156 | onMention({ 157 | user: { 158 | screen_name: 'louroboros', 159 | }, 160 | text: commandText, 161 | }); 162 | }); 163 | 164 | tweetGameBoard({ 165 | gameState: game.state, 166 | }).then((status) => { 167 | console.log(status); 168 | }, (err) => { 169 | throw err; 170 | }); 171 | -------------------------------------------------------------------------------- /src/parseCommands.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | // Matches one or more commands: 4 | 5 | let chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 6 | 7 | 8 | export default function parseCommands (params) { 9 | let { 10 | status, 11 | width, 12 | height, 13 | } = params; 14 | 15 | let xChars = chars.slice(0, width); 16 | let yChars = chars.slice(width, width + height); 17 | 18 | let results = []; 19 | 20 | let matcher = /(^(click|flag|unflag))*((click|flag|unflag)\s+)?([a-z])[\s:,;-]+([a-z])(^(click|flag|unflag))*/ig; 21 | 22 | let matches; 23 | while (matches = matcher.exec(status)) { 24 | let firstX = xChars.indexOf(matches[5].toUpperCase()); 25 | let firstY = yChars.indexOf(matches[5].toUpperCase()); 26 | 27 | let secondX = xChars.indexOf(matches[6].toUpperCase()); 28 | let secondY = yChars.indexOf(matches[6].toUpperCase()); 29 | 30 | let x, y; 31 | 32 | if (firstX > -1 && secondY > -1) { 33 | x = firstX; 34 | y = secondY; 35 | } else if (firstY > -1 && secondX > -1) { 36 | y = firstY; 37 | x = secondX; 38 | } else { 39 | return false; 40 | } 41 | 42 | results.push({ 43 | type: (matches[4] || 'click').toLowerCase(), 44 | x: x, 45 | y: y, 46 | }); 47 | } 48 | 49 | return results; 50 | }; 51 | 52 | let tests = [ 53 | [ 54 | { 55 | status: `@minetweeter_ click b N`, 56 | width: 9, 57 | height: 9, 58 | }, 59 | 60 | [ 61 | {type: 'click', x: 1, y: 4, }, 62 | ], 63 | ], 64 | 65 | [ 66 | { 67 | status: `@minetweeter_ click n B`, 68 | width: 9, 69 | height: 9, 70 | }, 71 | 72 | [ 73 | {type: 'click', x: 1, y: 4, }, 74 | ], 75 | ], 76 | 77 | [ 78 | { 79 | status: `@minetweeter_ flag X X`, 80 | width: 9, 81 | height: 9, 82 | }, 83 | 84 | false, 85 | ], 86 | 87 | [ 88 | { 89 | status: `@minetweeter_ flag A A`, 90 | width: 9, 91 | height: 9, 92 | }, 93 | 94 | false, 95 | ], 96 | 97 | [ 98 | { 99 | status: `@minetweeter_ unFLAG n b`, 100 | width: 9, 101 | height: 9, 102 | }, 103 | 104 | [ 105 | {type: 'unflag', x: 1, y: 4, }, 106 | ], 107 | ], 108 | 109 | [ 110 | { 111 | status: `@minetweeter_ flag a j unflag a j click a j`, 112 | width: 9, 113 | height: 9, 114 | }, 115 | 116 | [ 117 | {type: 'flag', x: 0, y: 0, }, 118 | {type: 'unflag', x: 0, y: 0, }, 119 | {type: 'click', x: 0, y: 0, }, 120 | ], 121 | ], 122 | 123 | [ 124 | { 125 | status: `@minetweeter_ a j unflag a j click a j`, 126 | width: 9, 127 | height: 9, 128 | }, 129 | 130 | [ 131 | {type: 'click', x: 0, y: 0, }, 132 | {type: 'unflag', x: 0, y: 0, }, 133 | {type: 'click', x: 0, y: 0, }, 134 | ], 135 | ], 136 | ]; 137 | 138 | function deepEquals (a, b) { 139 | return JSON.stringify(a) === JSON.stringify(b); 140 | } 141 | 142 | tests.forEach((test) => { 143 | let [input, expectedOutput] = test; 144 | 145 | let actualOutput = parseCommands(input); 146 | 147 | if (!deepEquals(expectedOutput, actualOutput)) { 148 | let message = `parseCommands test failed.\n\n` + 149 | `Input:\n"${input.status}"\n` + 150 | `Expected:\n${JSON.stringify(expectedOutput,null,2)}\n\n` + 151 | `...but got:\n${JSON.stringify(actualOutput,null,2)}\n\n`; 152 | throw new Error(message); 153 | } 154 | }); -------------------------------------------------------------------------------- /src/range.js: -------------------------------------------------------------------------------- 1 | import Immutable from 'immutable'; 2 | 3 | export default function range (start, end) { 4 | return Immutable.List(Immutable.Range(start, end)); 5 | }; -------------------------------------------------------------------------------- /src/shuffle.js: -------------------------------------------------------------------------------- 1 | export default function shuffle (list) { 2 | return list.reduce((result, currentValue, currentIndex) => { 3 | let randomIndex = Math.floor(Math.random() * currentIndex); 4 | let otherValue = result.get(randomIndex); 5 | 6 | return result.set(randomIndex, currentValue).set(currentIndex, otherValue); 7 | }, list); 8 | }; -------------------------------------------------------------------------------- /src/throttle.js: -------------------------------------------------------------------------------- 1 | import delay from './delay'; 2 | import Immutable from 'immutable'; 3 | 4 | export default function throttle (ms, func) { 5 | let q = Immutable.List(); 6 | 7 | let awake = false; 8 | async function awaken () { 9 | if (awake) { 10 | return; 11 | } 12 | 13 | awake = true; 14 | while (q.size > 0) { 15 | let { 16 | resolve, 17 | reject, 18 | args, 19 | } = q.first(); 20 | 21 | q = q.shift(); 22 | 23 | try { 24 | resolve(func(...args)); 25 | } catch (e) { 26 | console.error(e); 27 | reject(e); 28 | } 29 | 30 | await delay(ms); 31 | } 32 | 33 | awake = false; 34 | } 35 | 36 | let throttledFunc = function (...args) { 37 | return new Promise(function (resolve, reject) { 38 | q = q.push({ 39 | resolve: resolve, 40 | reject: reject, 41 | args: args, 42 | }); 43 | 44 | awaken(); 45 | }); 46 | }; 47 | 48 | throttledFunc.clearQueue = function clearQueue () { 49 | q = Immutable.List(); 50 | awake = false; 51 | }; 52 | 53 | return throttledFunc; 54 | }; -------------------------------------------------------------------------------- /src/toFullWidthString.js: -------------------------------------------------------------------------------- 1 | let keys = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()-=+<>.,;: '; 2 | let values = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()-=+<>.,;: '; 3 | 4 | let map = keys.split('').reduce((result, char, idx) => { 5 | result[char] = values[idx]; 6 | return result; 7 | }, {}); 8 | 9 | let begin = ''; 10 | 11 | export default function toFullWidthString (str) { 12 | return begin + str.split('').map((k) => { return map[k] || k; }).join(''); 13 | } -------------------------------------------------------------------------------- /src/twit.js: -------------------------------------------------------------------------------- 1 | import Twit from 'twit'; 2 | import twitSettings from '../twitSettings'; 3 | import throttle from './throttle'; 4 | 5 | let functionsToWrapInPromises = [ 6 | 'get', 7 | 'post', 8 | ]; 9 | 10 | let throttleDelay = (60*1000)/15; 11 | 12 | let twit = functionsToWrapInPromises.reduce((result, funcName) => { 13 | let func = result[funcName].bind(result); 14 | 15 | result[funcName] = throttle(throttleDelay, (...args) => { 16 | return new Promise (function (resolve, reject) { 17 | try { 18 | func(...args, (err, data, response) => { 19 | if (err) { 20 | console.error('ugh', err); 21 | return reject(err); 22 | } 23 | 24 | resolve(data); 25 | }); 26 | } catch (err) { 27 | console.error('unexpected error in twit', err); 28 | reject (err); 29 | } 30 | }); 31 | }); 32 | 33 | return result; 34 | }, new Twit(twitSettings)); 35 | 36 | export default twit; 37 | -------------------------------------------------------------------------------- /twitSettings.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "consumer_key": process.env.TWIT_CONSUMER_KEY, 3 | "consumer_secret": process.env.TWIT_CONSUMER_SECRET, 4 | "access_token": process.env.TWIT_ACCESS_TOKEN, 5 | "access_token_secret": process.env.TWIT_ACCESS_TOKEN_SECRET, 6 | }; --------------------------------------------------------------------------------