├── .gitignore ├── README.md ├── pom.xml ├── repository └── pircbot │ └── pircbot-ssl │ ├── 1.5.0 │ ├── pircbot-ssl-1.5.0.jar │ └── pircbot-ssl-1.5.0.pom │ └── maven-metadata-local.xml └── src ├── main ├── antlr3 │ └── com │ │ └── etsy │ │ └── pushbot │ │ ├── Command.g │ │ └── PushTrain.g ├── java │ └── com │ │ └── etsy │ │ └── pushbot │ │ ├── ChannelInfo.java │ │ ├── CommandReader.java │ │ ├── GraphiteLogger.java │ │ ├── Member.java │ │ ├── PushBot.java │ │ ├── PushTrain.java │ │ ├── PushTrainReader.java │ │ ├── command │ │ ├── AnnounceCommand.java │ │ ├── AtCommand.java │ │ ├── ConfigCommand.java │ │ ├── DoneCommand.java │ │ ├── DriveCommand.java │ │ ├── GoodCommand.java │ │ ├── HelpCommand.java │ │ ├── HoldCommand.java │ │ ├── JoinCommand.java │ │ ├── MessageCommand.java │ │ ├── NevermindCommand.java │ │ ├── PopCommand.java │ │ ├── SayCommand.java │ │ ├── TrainCommand.java │ │ ├── UhohCommand.java │ │ └── UnholdCommand.java │ │ ├── config │ │ ├── Config.java │ │ ├── ConfigDao.java │ │ ├── ConfigServer.java │ │ ├── Response.java │ │ ├── Status.java │ │ ├── StatusServlet.java │ │ └── api │ │ │ └── GetMemberConfig.java │ │ └── tokens │ │ ├── Hold.java │ │ ├── MemberList.java │ │ └── PushToken.java └── webapp │ ├── WEB-INF │ ├── lib │ │ ├── jersey-core-1.6.jar │ │ └── jersey-json-1.6.jar │ └── web.xml │ ├── css │ ├── buttons.less │ ├── definitions.less │ ├── style.less │ └── vendor │ │ └── reset-fonts-grids.css │ ├── index.html │ └── js │ ├── PushBotConfig.js │ ├── main.js │ └── vendor │ ├── allplugins-require.js │ ├── jquery-1.6.1.min.js │ ├── jquery.address-1.4.min.js │ ├── jquery.flot-0.1.pack.js │ ├── jquery.log.js │ ├── jquery.timeago.js │ ├── jquery.tmpl.min.js │ ├── jquery.validate.min.js │ ├── less-1.1.0.min.js │ ├── require.js │ ├── underscore-min.js │ └── underscore.string.min.js └── test └── java └── com └── etsy └── pushbot └── PushBotTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PushBot is an IRC bot that manages the topic in an IRC channel that has a push train. 2 | 3 | 4 | This is an archived project 5 | =========================== 6 | 7 | Pushbot is no longer actively maintained and is no longer in sync with the version used internally at Etsy. 8 | 9 | 10 | USAGE 11 | ===== 12 | 13 | .join -- Join the queue for a normal push wherever its convenient 14 | .join config -- Join the queue for a config push 15 | .join HOLD -- A HOLD that is queued and named 16 | .join HOLD "message" -- You can set a message when you join 17 | .join askme -- Join the queue and suggest that people ask you before joining along 18 | .join before USER -- Join before the given user 19 | .join with USER -- Join the queue at the first position with [username] 20 | .join config with USER -- You can string join commands together 21 | .join last -- Join at the end of the queue 22 | .at {commit,dev,...} -- Set the state of your push to being at the given value 23 | .in -- Mark yourself as having your code checked in 24 | .good -- Mark yourself as all-good in the current push state 25 | .uhoh -- Mark yourself as not-all-good in the current push state 26 | .done -- Mark the head of the push queue as done 27 | .nevermind -- Hop out of the queue 28 | .pop -- Remove your last entry in the queue 29 | .hold "message" -- Set a hold with a message. Don't forget the quotes. 30 | .unhold -- Release the hold 31 | .message "message" -- Set a message. Don't forget the quotes. 32 | .message - -- Remove the message 33 | .kick username -- Punt someone from the queue 34 | .drive -- Make yourself the leader of the first push group you're in 35 | .config -- Get a link to the PushBot settings page 36 | .help -- Show Help Information 37 | 38 | Join #pushbot to play around with pushbot and see how it works. 39 | 40 | An Example 41 | ========== 42 | 43 | Let's say you're in an IRC channel named #push and it has the initial topic "clear". PushBot 44 | can help organize a push queue. 45 | 46 | TOPIC: clear 47 | alice> .join TOPIC: alice 48 | bob> .join with alice TOPIC: alice + bob 49 | pushbot> alice, bob: You're up TOPIC: alice + bob 50 | bob> .good TOPIC: alice + bob* 51 | alice> .good TOPIC: alice* + bob* 52 | pushbot> alice, bob: Everyone is ready TOPIC: alice* + bob* 53 | carol> .join TOPIC: alice* + bob* | carol 54 | alice> .at preprod TOPIC: alice + bob | carol 55 | alice> .good TOPIC: alice* + bob | carol 56 | bob> .good TOPIC: alice* + bob* | carol 57 | pushbot> alice, bob: Everyone is ready TOPIC: alice* + bob* | carol 58 | alice> .at prod TOPIC: alice + bob | carol 59 | alice> .good TOPIC: alice* + bob | carol 60 | dave> .join TOPIC: alice* + bob | carol + dave 61 | bob> .good TOPIC: alice* + bob* | carol + dave 62 | pushbot> alice, bob: Everyone is ready TOPIC: alice* + bob* | carol + dave 63 | alice> .done TOPIC: carol + dave 64 | pushbot> carol, dave: You're up TOPIC: carol + dave 65 | 66 | 67 | Configuring PushBot For Your Handle 68 | =================================== 69 | 70 | You can modify a few settings within PushBot with respect to your IRC handle. 71 | 72 | * You can tell PushBot to try to be quiet when you're driving 73 | * You can have PushBot send you Notifo notifications when you're at the head of the queue 74 | 75 | To configure PushBot, head to http://[pushbot-hostname]:8080/ 76 | 77 | Tricks For Suppressing Pushbot Topic Change Spam 78 | ================================================ 79 | 80 | ### Colloquy 81 | 82 | Edit the CSS for your chosen style. If you're using "DecafBland - Inverted", for instance, you should open the file 83 | 84 | /Applications/Colloquy.app/Contents/Resources/Styles/DecafBland.colloquyStyle/Contents/Resources/Variants/Inverted.css 85 | 86 | and add the line 87 | 88 | .event { display: none; } 89 | 90 | That'll get rid of all event messages (joins, parts, topic changes), and could be too much, so you might want to make a variant of your style just for #push. 91 | 92 | ### Limechat 93 | 94 | Edit the CSS for your chosen style, for instance 95 | 96 | /Applications/LimeChat.app/Contents/Themes/Limelight.css 97 | 98 | and add the lines 99 | 100 | html[channelname="#push"] div[type=topic] { 101 | position: fixed; 102 | top: 0; 103 | left: 0; 104 | padding-left: 0 !important; 105 | background: #000; 106 | width: 100%; 107 | } 108 | 109 | That'll move all topics in the #push channel to a line on the top of the channel, with new topics covering up old topics. 110 | 111 | ### IRSSI 112 | 113 | /ignore -channels #push * TOPICS 114 | 115 | ### WeeChat 116 | 117 | /filter add hush_pushbot irc.host.#push irc_topic pushbot 118 | 119 | ### Other Clients 120 | 121 | If you have instructions for other clients, send them to me and I'll add them. 122 | 123 | 124 | Hacking 125 | ======= 126 | 127 | * The bot and its config lives in [src/main/java/com/etsy/pushbot/PushBot.java](https://github.com/Etsy/PushBot/blob/master/src/main/java/com/etsy/pushbot/PushBot.java "PushBot.java") 128 | * The topic grammar lives in [src/main/antlr3/com/etsy/pushbot/PushTrain.g](https://github.com/Etsy/PushBot/blob/master/src/main/antlr3/com/etsy/pushbot/PushTrain.g "PushTrain.g") 129 | * The command grammar lives in [src/main/antlr3/com/etsy/pushbot/Command.g](https://github.com/Etsy/PushBot/blob/master/src/main/antlr3/com/etsy/pushbot/Command.g "Command.g") 130 | 131 | To build and run PushBot, run 132 | 133 | > cd PushBot 134 | > mvn test 135 | > mvn package 136 | > java -jar target/PushBot.jar --name pushbot --channels "#push,#pushbot" --irc-host "irc.network.net" --irc-port 6667 --irc-pass "password" 137 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.etsy.pushbot 5 | PushBot 6 | 1.0 7 | jar 8 | PushBot 9 | http://github.com/Etsy/PushBot 10 | 11 | UTF-8 12 | 1.6 13 | 14 | 15 | 16 | pircbot 17 | file://${project.basedir}/repository/ 18 | 19 | 20 | 21 | 22 | 23 | junit 24 | junit 25 | 3.8.1 26 | test 27 | 28 | 35 | 36 | pircbot 37 | pircbot-ssl 38 | 1.5.0 39 | 40 | 41 | 42 | 43 | org.antlr 44 | antlr 45 | 3.4 46 | 47 | 48 | com.google.guava 49 | guava 50 | r05 51 | 52 | 53 | org.codehaus.jackson 54 | jackson-jaxrs 55 | 1.6.2 56 | 57 | 58 | com.sun.jersey 59 | jersey-server 60 | ${jersey-version} 61 | 62 | 63 | com.sun.jersey 64 | jersey-json 65 | ${jersey-version} 66 | 67 | 68 | com.sun.jersey 69 | jersey-client 70 | ${jersey-version} 71 | test 72 | 73 | 74 | javax.servlet 75 | servlet-api 76 | 2.5 77 | 78 | 79 | org.eclipse.jetty 80 | jetty-webapp 81 | 7.5.1.v20110908 82 | 83 | 84 | org.mortbay.jetty 85 | jsp-2.1-glassfish 86 | 2.1.v20100127 87 | 88 | 89 | commons-codec 90 | commons-codec 91 | 1.4 92 | 93 | 94 | commons-lang 95 | commons-lang 96 | 2.5 97 | 98 | 99 | commons-cli 100 | commons-cli 101 | 1.2 102 | 103 | 104 | commons-httpclient 105 | commons-httpclient 106 | 3.1 107 | 108 | 109 | org.xerial 110 | sqlite-jdbc 111 | 3.6.10 112 | 113 | 114 | com.github.sps.notifo4j 115 | notifo-client 116 | 1.2 117 | 118 | 119 | 120 | PushBot 121 | 122 | 123 | org.apache.maven.plugins 124 | maven-compiler-plugin 125 | 2.0.2 126 | 127 | 1.6 128 | 1.6 129 | 130 | 131 | 132 | org.antlr 133 | antlr3-maven-plugin 134 | 3.4 135 | 136 | 137 | 138 | antlr 139 | 140 | 141 | target/generated-sources 142 | 143 | 144 | 145 | 146 | 147 | maven-assembly-plugin 148 | 2.2.1 149 | 150 | 151 | PushBot 152 | package 153 | 154 | single 155 | 156 | 157 | false 158 | 159 | jar-with-dependencies 160 | 161 | 162 | 163 | com.etsy.pushbot.PushBot 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | org.apache.maven.plugins 172 | maven-jar-plugin 173 | 3.0.0 174 | 175 | 176 | 177 | true 178 | com.etsy.pushbot.PushBot 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | -------------------------------------------------------------------------------- /repository/pircbot/pircbot-ssl/1.5.0/pircbot-ssl-1.5.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etsy/PushBot/17e1b3070d395e3644037fc9091ec4d3195dd4f4/repository/pircbot/pircbot-ssl/1.5.0/pircbot-ssl-1.5.0.jar -------------------------------------------------------------------------------- /repository/pircbot/pircbot-ssl/1.5.0/pircbot-ssl-1.5.0.pom: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | pircbot 6 | pircbot-ssl 7 | 1.5.0 8 | POM was created from install:install-file 9 | 10 | -------------------------------------------------------------------------------- /repository/pircbot/pircbot-ssl/maven-metadata-local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | pircbot 4 | pircbot-ssl 5 | 6 | 1.5.0 7 | 8 | 1.5.0 9 | 10 | 20160601161756 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/antlr3/com/etsy/pushbot/Command.g: -------------------------------------------------------------------------------- 1 | grammar Command; 2 | 3 | @header { 4 | package com.etsy.pushbot; 5 | 6 | import com.etsy.pushbot.command.*; 7 | import java.util.LinkedList; 8 | } 9 | 10 | @lexer::header { 11 | package com.etsy.pushbot; 12 | import java.util.LinkedList; 13 | } 14 | 15 | @parser::members { 16 | @Override 17 | public void reportError(RecognitionException e) { 18 | return; 19 | } 20 | } 21 | 22 | @lexer::members { 23 | protected void mismatch(IntStream input, int ttype, BitSet follow) 24 | throws RecognitionException 25 | { 26 | throw new MismatchedTokenException(ttype, input); 27 | } 28 | 29 | public Object recoverFromMismatchedSet(IntStream input, RecognitionException e, BitSet follow) 30 | throws RecognitionException 31 | { 32 | throw e; 33 | } 34 | } 35 | 36 | @rulecatch { 37 | catch (RecognitionException e) { 38 | throw e; 39 | } 40 | } 41 | 42 | commands returns [ List value ] 43 | : { value = new LinkedList(); } 44 | token=command WHITESPACE? { value.add($token.value); } 45 | ( COMMAND_DELIMITER 46 | WHITESPACE? 47 | token=command { value.add($token.value); } )* 48 | ; 49 | 50 | command returns [ TrainCommand value ] 51 | : PB join { $value = new JoinCommand(); } 52 | ( WHITESPACE type { ((JoinCommand)$value).setType($type.text); } )? 53 | ( WHITESPACE with WHITESPACE w=member { ((JoinCommand)$value).setWith($w.text); } )? 54 | ( WHITESPACE before WHITESPACE b=member { ((JoinCommand)$value).setBefore($b.text); } )? 55 | ( WHITESPACE last { ((JoinCommand)$value).setLast(true); } )? 56 | ( WHITESPACE MESSAGE_STRING { ((JoinCommand)$value).setMessage($MESSAGE_STRING.text); })? 57 | | PB done 58 | { $value = new DoneCommand(); } 59 | | PB 'pop' 60 | { $value = new PopCommand(); } 61 | | PB 'hold' { $value = new HoldCommand(); } 62 | (WHITESPACE MESSAGE_STRING { ((HoldCommand)$value).setMessage($MESSAGE_STRING.text); })? 63 | | PB 'unhold' 64 | { $value = new UnholdCommand(); } 65 | | PB nevermind 66 | { $value = new NevermindCommand(); } 67 | | PB 'message' WHITESPACE MESSAGE_STRING 68 | { $value = new MessageCommand($MESSAGE_STRING.text); } 69 | | PB 'message' WHITESPACE MESSAGE_CLEAR 70 | { $value = new MessageCommand($MESSAGE_CLEAR.text); } 71 | | PB 'at' WHITESPACE state 72 | { $value = new AtCommand($state.text, null); } 73 | | PB good 74 | { $value = new GoodCommand(); } 75 | | PB 'say' { $value = new SayCommand(); } 76 | (WHITESPACE MESSAGE_STRING { ((SayCommand)$value).setMessage($MESSAGE_STRING.text); })? 77 | | PB 'drive' 78 | { $value = new DriveCommand(); } 79 | | PB uhoh 80 | { $value = new UhohCommand(); } 81 | | PB 'help' 82 | { $value = new HelpCommand(); } 83 | | PB 'config' 84 | { $value = new ConfigCommand(); } 85 | | PB 'kick' WHITESPACE member 86 | { $value = new NevermindCommand(); ((NevermindCommand)$value).setMember($member.text); } 87 | ; 88 | 89 | member 90 | : STRING 91 | ; 92 | 93 | with 94 | : 'with' 95 | | 'con' 96 | ; 97 | 98 | before 99 | : 'before' 100 | | 'antes' 101 | ; 102 | 103 | last 104 | : 'last' 105 | | 'ultimo' 106 | ; 107 | 108 | good 109 | : 'good' 110 | | 'great' 111 | | 'in' 112 | | 'go' 113 | | 'si' 114 | | 'bueno' 115 | | 'listo' 116 | ; 117 | 118 | state 119 | : 'commit' 120 | | 'push' 121 | | 'trunk' 122 | | 'qa' 123 | | 'dev' 124 | | 'princess' 125 | | 'preprod' 126 | | 'prod' 127 | | 'production' 128 | ; 129 | 130 | type 131 | : 'config' 132 | | 'askme' 133 | | 'hold' 134 | | 'HOLD' 135 | ; 136 | 137 | done 138 | : 'done' 139 | | 'finito' 140 | ; 141 | 142 | join 143 | : 'join' 144 | | 'unir' 145 | ; 146 | 147 | uhoh 148 | : 'uhoh' 149 | | 'notgood' 150 | | 'not_good' 151 | | 'bad' 152 | | 'fml' 153 | | 'fuck' 154 | | 'fucked' 155 | ; 156 | 157 | nevermind 158 | : 'nevermind' 159 | | 'nm' 160 | | 'olvidate' 161 | ; 162 | 163 | WHITESPACE 164 | : (' '|'\t'|'\n'|'\r')* 165 | ; 166 | 167 | STRING 168 | : ('a'..'z'|'A'..'Z'|'0'..'9'|'_')+ 169 | ; 170 | 171 | MESSAGE_STRING 172 | : '"' .* '"' 173 | ; 174 | 175 | MESSAGE_CLEAR 176 | : '-' 177 | ; 178 | 179 | MEMBER_DELIMITER 180 | : ('+'|',') 181 | ; 182 | 183 | QUEUE_DELIMITER 184 | : ('|') 185 | ; 186 | 187 | COMMAND_DELIMITER 188 | : ';' 189 | ; 190 | 191 | PB : '.' ; 192 | -------------------------------------------------------------------------------- /src/main/antlr3/com/etsy/pushbot/PushTrain.g: -------------------------------------------------------------------------------- 1 | grammar PushTrain; 2 | 3 | @header { 4 | package com.etsy.pushbot; 5 | 6 | import com.etsy.pushbot.tokens.*; 7 | import java.util.LinkedList; 8 | } 9 | 10 | @lexer::header { 11 | package com.etsy.pushbot; 12 | } 13 | 14 | @parser::members { 15 | @Override 16 | public void reportError(RecognitionException e) { 17 | return; 18 | } 19 | } 20 | 21 | @lexer::members { 22 | protected void mismatch(IntStream input, int ttype, BitSet follow) 23 | throws RecognitionException 24 | { 25 | throw new MismatchedTokenException(ttype, input); 26 | } 27 | 28 | public Object recoverFromMismatchedSet(IntStream input, RecognitionException e, BitSet follow) 29 | throws RecognitionException 30 | { 31 | throw e; 32 | } 33 | } 34 | 35 | @rulecatch { 36 | catch (RecognitionException e) { 37 | throw e; 38 | } 39 | } 40 | 41 | 42 | push_train returns [ PushTrain pushTrain ] 43 | : { pushTrain = new PushTrain(); } 44 | token=push_token { pushTrain.add($token.value); } 45 | ( WHITESPACE? 46 | QUEUE_DELIMITER 47 | WHITESPACE? 48 | token=push_token { pushTrain.add($token.value); } 49 | )* 50 | ( message_string { pushTrain.setMessage($message_string.value); } )? 51 | | clear? { pushTrain = new PushTrain(); } 52 | ( message_string { pushTrain.setMessage($message_string.value); } )? 53 | | WHITESPACE? { pushTrain = new PushTrain(); } 54 | ; 55 | 56 | push_token returns [ PushToken value ] 57 | : hold { $value = $hold.value; } 58 | | member_list { $value = $member_list.value; } 59 | ; 60 | 61 | hold returns [ Hold value ] 62 | : 'HOLD' { $value = new Hold(); } 63 | ( WHITESPACE? MESSAGE { $value.setMessage($MESSAGE.text); })? 64 | ; 65 | 66 | member_list returns [ MemberList value ] 67 | : { $value = new MemberList(); } 68 | ( '<' STRING '>' { $value.setState($STRING.text); } WHITESPACE )? 69 | m=member { $value.add($m.value); } 70 | ( WHITESPACE? 71 | MEMBER_DELIMITER 72 | WHITESPACE? 73 | m=member { $value.add($m.value); } 74 | )* 75 | ( WHITESPACE MESSAGE { $value.setMessage($MESSAGE.text); })? 76 | ; 77 | 78 | member returns [ Member value ] 79 | : STRING { $value = new Member($STRING.text); } 80 | ( status { $value.setStatus($status.text); } )? 81 | ( WHITESPACE? '(' type ')' { $value.setType($type.text); } )? 82 | ; 83 | 84 | message_string returns [ String value ] 85 | : WHITESPACE 86 | MESSAGE_DELIMITER 87 | WHITESPACE 88 | MESSAGE 89 | { $value = $MESSAGE.text; } 90 | ; 91 | 92 | clear 93 | : 'clear' 94 | ; 95 | 96 | status 97 | : '*' 98 | | '!' 99 | | '~' 100 | ; 101 | 102 | type 103 | : 'config' 104 | | 'askme' 105 | | 'hold' 106 | | 'HOLD' 107 | ; 108 | 109 | WHITESPACE 110 | : (' '|'\t'|'\n'|'\r')* 111 | ; 112 | 113 | STRING 114 | : ('a'..'z'|'A'..'Z'|'0'..'9'|'_'|'-')+ 115 | ; 116 | 117 | MEMBER_DELIMITER 118 | : ('+'|','|'&') 119 | ; 120 | 121 | QUEUE_DELIMITER 122 | : '|' 123 | ; 124 | 125 | MESSAGE_DELIMITER 126 | : '!!' 127 | ; 128 | 129 | MESSAGE 130 | : '"' .* '"' 131 | ; 132 | 133 | 134 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/ChannelInfo.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot; 2 | 3 | import java.util.Date; 4 | 5 | public class ChannelInfo { 6 | 7 | private String name; 8 | private volatile String topic; 9 | private Date topicSetDate; 10 | private boolean hasBadTopic = false; 11 | 12 | public ChannelInfo(String name) { 13 | this.name = name; 14 | } 15 | 16 | public String getTopic() { 17 | if(topic == null) { 18 | return ""; 19 | } 20 | return topic; 21 | } 22 | 23 | public void setTopic(String topic) { 24 | this.topic = topic; 25 | this.topicSetDate = new Date(); 26 | } 27 | 28 | /** 29 | * Set to true to indicate that the current topic is 30 | * unparseable and no commands should be accepted until the 31 | * topic is manually fixed 32 | */ 33 | public void setHasBadTopic(boolean hasBadTopic) { 34 | this.hasBadTopic = hasBadTopic; 35 | } 36 | 37 | public boolean getHasBadTopic() { 38 | return this.hasBadTopic; 39 | } 40 | 41 | public int getTopicAgeInSeconds() { 42 | Date now = new Date(); 43 | return (int)((now.getTime() - this.topicSetDate.getTime())/1000); 44 | } 45 | 46 | public PushTrain getPushTrain() { 47 | if(topic == null) { 48 | return null; 49 | } 50 | return PushTrainReader.parse(topic); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/CommandReader.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot; 2 | 3 | import com.etsy.pushbot.command.TrainCommand; 4 | import java.io.ByteArrayInputStream; 5 | import java.util.List; 6 | import org.antlr.runtime.CommonTokenStream; 7 | import org.antlr.runtime.ANTLRInputStream; 8 | 9 | public class CommandReader { 10 | 11 | private CommandReader() {} 12 | 13 | public static List parse(String message) { 14 | try { 15 | ANTLRInputStream input = 16 | new ANTLRInputStream(new ByteArrayInputStream(message.getBytes("UTF-8"))); 17 | 18 | CommandLexer commandLexer = 19 | new CommandLexer(input); 20 | 21 | CommonTokenStream tokens = 22 | new CommonTokenStream(commandLexer); 23 | 24 | CommandParser commandParser = 25 | new CommandParser(tokens); 26 | 27 | return commandParser.commands(); 28 | 29 | } catch(Exception e) { 30 | // hmm 31 | } 32 | 33 | return null; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/GraphiteLogger.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Taken from 3 | * http://neopatel.blogspot.com/2011/04/logging-to-graphite-monitoring-tool.html 4 | */ 5 | package com.etsy.pushbot; 6 | 7 | import java.io.OutputStreamWriter; 8 | import java.io.Writer; 9 | import java.net.Socket; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | 14 | public class GraphiteLogger { 15 | private String graphiteHost; 16 | private int graphitePort; 17 | 18 | public GraphiteLogger(String graphiteHost, 19 | int graphitePort) { 20 | this.graphiteHost = graphiteHost; 21 | this.graphitePort = graphitePort; 22 | } 23 | 24 | public String getGraphiteHost() { 25 | return graphiteHost; 26 | } 27 | 28 | public void setGraphiteHost(String graphiteHost) { 29 | this.graphiteHost = graphiteHost; 30 | } 31 | 32 | public int getGraphitePort() { 33 | return graphitePort; 34 | } 35 | 36 | public void setGraphitePort(int graphitePort) { 37 | this.graphitePort = graphitePort; 38 | } 39 | 40 | public void logToGraphite(String key, long value) { 41 | Map stats = new HashMap(); 42 | stats.put(key, value); 43 | logToGraphite(stats); 44 | } 45 | 46 | public void logToGraphite(Map stats) { 47 | if (stats.isEmpty()) { 48 | return; 49 | } 50 | 51 | try { 52 | logToGraphite("ci.push", stats); 53 | } catch (Throwable t) { 54 | System.out.println("Can't log to graphite " + t.getMessage()); 55 | } 56 | } 57 | 58 | private void logToGraphite(String nodeIdentifier, Map stats) throws Exception { 59 | Long curTimeInSec = System.currentTimeMillis() / 1000; 60 | StringBuffer lines = new StringBuffer(); 61 | for (Map.Entry entry : stats.entrySet()) { 62 | String key = nodeIdentifier + "." + entry.getKey(); 63 | lines.append(key).append(" ").append(entry.getValue()).append(" ").append(curTimeInSec).append("\n"); //even the last line in graphite 64 | } 65 | logToGraphite(lines); 66 | } 67 | 68 | private void logToGraphite(StringBuffer lines) throws Exception { 69 | String msg = lines.toString(); 70 | System.out.println("Writing [{}] to graphite " + msg); 71 | Socket socket = new Socket(graphiteHost, graphitePort); 72 | try { 73 | Writer writer = new OutputStreamWriter(socket.getOutputStream()); 74 | writer.write(msg); 75 | writer.flush(); 76 | writer.close(); 77 | } finally { 78 | socket.close(); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/Member.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot; 2 | 3 | import com.etsy.pushbot.config.Config; 4 | import com.etsy.pushbot.config.ConfigDao; 5 | import com.notifo.client.NotifoClient; 6 | import com.notifo.client.NotifoHttpClient; 7 | import com.notifo.client.NotifoMessage; 8 | 9 | public class Member { 10 | 11 | private String name; 12 | private String type = null; 13 | private String status = null; 14 | 15 | public Member(String name) { 16 | this.name = name; 17 | } 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public void setType(String type) { 24 | this.type = type; 25 | } 26 | 27 | public String getType() { 28 | return type; 29 | } 30 | 31 | public boolean isDark() { 32 | return type != null && "dark".equals(type); 33 | } 34 | 35 | public boolean isConfig() { 36 | return type != null && "config".equals(type); 37 | } 38 | 39 | public void setStatus(String status) { 40 | this.status = status; 41 | } 42 | 43 | public String getStatus() { 44 | return this.status; 45 | } 46 | 47 | public Config getConfig() { 48 | try { 49 | return ConfigDao.getInstance().getConfigForMember(this.getName()); 50 | } 51 | catch(Throwable t) { 52 | System.err.println(t.getMessage()); 53 | t.printStackTrace(); 54 | } 55 | return new Config(); 56 | } 57 | 58 | public void onHeadOfQueue(PushBot pushBot, PushTrain pushTrain, String channel) { 59 | if(getConfig().sendNotifoWhenUp) { 60 | String notifoUsername = getConfig().notifoUsername; 61 | String notifoApiSecret = getConfig().notifoApiSecret; 62 | 63 | try { 64 | NotifoClient client = 65 | new NotifoHttpClient(notifoUsername, 66 | notifoApiSecret); 67 | 68 | NotifoMessage message = 69 | new NotifoMessage(notifoUsername, 70 | "You're at the head of the push queue."); 71 | 72 | client.sendMessage(message); 73 | } 74 | catch(Throwable t) { 75 | System.err.println(t.getMessage()); 76 | } 77 | } 78 | } 79 | 80 | @Override 81 | public String toString() { 82 | String s = name; 83 | if(status != null) { 84 | s += status; 85 | } 86 | if(type != null) { 87 | s += " (" + type + ")"; 88 | } 89 | return s; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/PushBot.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot; 2 | 3 | import com.etsy.pushbot.command.TrainCommand; 4 | import com.etsy.pushbot.config.ConfigServer; 5 | import com.etsy.pushbot.config.Status; 6 | import java.util.HashMap; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | import javax.net.ssl.SSLSocketFactory; 10 | import org.apache.commons.cli.CommandLine; 11 | import org.apache.commons.cli.CommandLineParser; 12 | import org.apache.commons.cli.Option; 13 | import org.apache.commons.cli.Options; 14 | import org.apache.commons.cli.ParseException; 15 | import org.apache.commons.cli.PosixParser; 16 | import org.jibble.pircbot.PircBot; 17 | 18 | /** 19 | * This is the main entry-point for PushBot 20 | */ 21 | public class PushBot extends PircBot 22 | { 23 | HashMap channelInfoMap = 24 | new HashMap(); 25 | 26 | private GraphiteLogger graphiteLogger = null; 27 | 28 | private final String primaryChannel; 29 | 30 | private List channels = new LinkedList(); 31 | 32 | /** 33 | * @param name 34 | * The nick of this bot 35 | * 36 | * @param channels 37 | * A list of channel names (each prefixed with '#') 38 | * 39 | * @param ircHost 40 | * The hostname of the iRCd server to connect to 41 | * 42 | * @param ircPort 43 | * The port that IRCd is listening on 44 | * 45 | * @param ircPassword 46 | * If non-null, the given password will be used when connecting to 47 | * IRCd 48 | * 49 | * @param graphiteEnabled 50 | * If true, stats will be logged to a graphite server 51 | * 52 | * @param graphiteHost 53 | * An optional hostname where stats will be logged (if enabled) 54 | * 55 | * @param graphitePort 56 | * An optional port that graphite is listening on 57 | */ 58 | public PushBot(String name, List channels, 59 | boolean graphiteEnabled, String graphiteHost, int graphitePort) { 60 | super(); 61 | 62 | // Configure pushbot's identity 63 | setName(name); 64 | setFinger(name); 65 | setLogin(name); 66 | setVerbose(true); 67 | 68 | // Configure pushbot's channels 69 | this.channels = channels; 70 | this.primaryChannel = channels.get(0); 71 | 72 | // Configure graphite logging 73 | if(graphiteEnabled) { 74 | graphiteLogger = new GraphiteLogger(graphiteHost, graphitePort); 75 | } 76 | } 77 | 78 | 79 | @Override 80 | protected void onConnect() { 81 | for(String channel : channels) { 82 | joinChannel(channel); 83 | } 84 | } 85 | 86 | @Override 87 | protected void onJoin(String channel, String sender, String login, String hostname) { 88 | } 89 | 90 | @Override 91 | protected void onDisconnect() { 92 | try { 93 | reconnect(); 94 | } catch(Exception e) { 95 | e.printStackTrace(); 96 | } 97 | } 98 | 99 | /** 100 | * 101 | */ 102 | public Status getStatus(String channel) 103 | throws Exception { 104 | 105 | Status status = new Status(); 106 | 107 | String topic = getTopic(channel); 108 | PushTrain pushTrain = PushTrainReader.parse(topic); 109 | 110 | status.isHold = pushTrain.isHold(); 111 | if(!status.isHold) { 112 | status.driver = pushTrain.getDriver().toString(); 113 | status.head = pushTrain.getHeadMember().trim(); 114 | } 115 | status.memberCount = pushTrain.getMemberCount(); 116 | 117 | status.isEveryoneReady = pushTrain.isHeadReady(); 118 | 119 | status.headState = pushTrain.getHeadState(); 120 | 121 | return status; 122 | } 123 | 124 | @Override 125 | protected void onTopic(String channel, String topic, String setBy, long date, boolean changed) { 126 | 127 | ChannelInfo channelInfo = channelInfoMap.get(channel); 128 | if(channelInfo == null) { 129 | channelInfoMap.put(channel, new ChannelInfo(channel)); 130 | channelInfo = channelInfoMap.get(channel); 131 | } 132 | 133 | PushTrain previousPushTrain = channelInfo.getPushTrain(); 134 | 135 | PushTrain newPushTrain; 136 | try { 137 | newPushTrain = PushTrainReader.parse(topic); 138 | if(newPushTrain == null) { 139 | channelInfo.setHasBadTopic(true); 140 | sendMessage(channel, "Sorry, I don't understand the current topic"); 141 | sendMessage(channel, "I won't accept new commands until the topic is fixed."); 142 | return; 143 | } 144 | channelInfo.setHasBadTopic(false); 145 | } catch(Throwable t) { 146 | t.printStackTrace(); 147 | return; 148 | } 149 | 150 | if(previousPushTrain != null && newPushTrain != null 151 | && !newPushTrain.getHeadMember() 152 | .equals(previousPushTrain.getHeadMember()) 153 | && !newPushTrain.getHeadMember().equals("clear")) { 154 | 155 | newPushTrain.onNewHead(this, channel, setBy); 156 | } 157 | 158 | if(graphiteLogger != null) { 159 | graphiteLogger.logToGraphite(channel+".queueSize", 160 | newPushTrain.size()); 161 | 162 | graphiteLogger.logToGraphite(channel+".members", 163 | newPushTrain.getMemberCount()); 164 | } 165 | 166 | channelInfo.setTopic(topic); 167 | } 168 | 169 | protected String getTopic(String channel) throws RuntimeException { 170 | if(channelInfoMap.get(channel) == null) { 171 | return null; 172 | } 173 | ChannelInfo channelInfo = channelInfoMap.get(channel); 174 | if(channelInfo.getHasBadTopic()) { 175 | throw new RuntimeException("Bad Topic"); 176 | } 177 | return channelInfo.getTopic(); 178 | } 179 | 180 | @Override 181 | protected synchronized void onMessage(String channel, String sender, String login, String hostname, String message) { 182 | 183 | List trainCommands; 184 | try { 185 | trainCommands = CommandReader.parse(message); 186 | if(trainCommands == null || trainCommands.size() == 0) { 187 | return; 188 | } 189 | } catch(Throwable t) { 190 | t.printStackTrace(); 191 | return; 192 | } 193 | 194 | String topic = null; 195 | try { 196 | topic = getTopic(channel); 197 | } 198 | catch(Exception exception) { 199 | sendMessage(channel, "Sorry, I don't understand the current topic"); 200 | return; 201 | } 202 | 203 | PushTrain pushTrain; 204 | try { 205 | pushTrain = PushTrainReader.parse(topic); 206 | if(pushTrain == null) { 207 | sendMessage(channel, "Sorry, I don't understand the current topic"); 208 | return; 209 | } 210 | } catch(Throwable t) { 211 | t.printStackTrace(); 212 | return; 213 | } 214 | 215 | for(TrainCommand trainCommand : trainCommands) { 216 | trainCommand.onCommand(this, pushTrain, channel, sender); 217 | } 218 | 219 | log(pushTrain.toString()); 220 | 221 | if(!pushTrain.toString().equals(topic)) { 222 | setTopic(channel, pushTrain.toString()); 223 | } 224 | } 225 | 226 | @Override 227 | protected void onPrivateMessage(String sender, String login, String hostname, String message) { 228 | onMessage(this.primaryChannel, sender, login, hostname, message); 229 | } 230 | 231 | @Override 232 | protected void onInvite(String targetNick, String sourceNick, String sourceLogin, String sourceHost, String channel) { 233 | joinChannel(channel); 234 | } 235 | 236 | public static void main(String[] args) 237 | throws Exception 238 | { 239 | Options options = new Options(); 240 | Option option; 241 | 242 | option = new Option("n", "name", true, "Name"); 243 | option.setRequired(true); 244 | options.addOption(option); 245 | 246 | option = new Option("c", "channels", true, "A comma delimited set of channels to join"); 247 | option.setRequired(true); 248 | options.addOption(option); 249 | 250 | option = new Option("h", "irc-host", true, "The IRCD host"); 251 | option.setRequired(true); 252 | options.addOption(option); 253 | 254 | option = new Option("p", "irc-port", true, "The IRCD port"); 255 | option.setRequired(true); 256 | options.addOption(option); 257 | 258 | option = new Option("a", "irc-pass", true, "IRCd Server password"); 259 | option.setRequired(false); 260 | options.addOption(option); 261 | 262 | option = new Option("s", "ssl", false, "Use SSL"); 263 | option.setRequired(false); 264 | options.addOption(option); 265 | 266 | option = new Option("g", "graphite-enabled", false, "Set to true to log stats to graphite"); 267 | options.addOption(option); 268 | 269 | option = new Option("r", "graphite-host", true, "Graphite server hostname"); 270 | options.addOption(option); 271 | 272 | option = new Option("t", "graphite-port", true, "Graphite server port"); 273 | options.addOption(option); 274 | 275 | CommandLineParser parser = new PosixParser(); 276 | CommandLine commandLine = null; 277 | try { 278 | commandLine = parser.parse(options, args); 279 | } 280 | catch(ParseException exception) { 281 | System.err.println(exception.getMessage()); 282 | System.err.println("Usage: " + PushBot.class + " ARGS"); 283 | System.err.println(" -n,--name IRC Nick of the Bot"); 284 | System.err.println(" -c,--channels Comma delimited list of channels to join"); 285 | System.err.println(" -h,--irc-host IRCD hostname"); 286 | System.err.println(" -p,--irc-port IRCD port"); 287 | System.err.println(" -a,--irc-passwod Optional IRCD server password"); 288 | System.err.println(" -s,--ssl Connect using SSL"); 289 | System.err.println(" -g,--graphite-enabled Enable graphite logging"); 290 | System.err.println(" -r,--graphite-host Graphite hostname"); 291 | System.err.println(" -t,--graphite-port Graphite port"); 292 | System.exit(1); 293 | } 294 | 295 | List channels = new LinkedList(); 296 | for(String channel : commandLine.getOptionValue('c').split("/\\s*,\\s*/")) { 297 | channels.add(channel); 298 | } 299 | 300 | // Build PushBot 301 | PushBot pushBot = 302 | new PushBot(commandLine.getOptionValue('n'), 303 | channels, 304 | commandLine.hasOption('g'), 305 | commandLine.getOptionValue('r', null), 306 | Integer.valueOf(commandLine.getOptionValue('t', "2003"))); 307 | 308 | SSLSocketFactory socketFactory = null; 309 | 310 | if (commandLine.hasOption('s')) { 311 | socketFactory = (SSLSocketFactory)SSLSocketFactory.getDefault(); 312 | } 313 | 314 | // Connect to IRCD 315 | pushBot.connect( 316 | commandLine.getOptionValue('h'), 317 | Integer.valueOf(commandLine.getOptionValue('p')), 318 | commandLine.getOptionValue('a', null), 319 | socketFactory 320 | ); 321 | 322 | // Launch the web interface 323 | ConfigServer configServer = new ConfigServer(pushBot); 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/PushTrain.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot; 2 | 3 | import com.etsy.pushbot.tokens.Hold; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | import java.util.LinkedList; 7 | import com.google.common.base.Joiner; 8 | 9 | public class PushTrain extends LinkedList 10 | { 11 | private String message; 12 | 13 | public PushTrain() { 14 | super(); 15 | } 16 | 17 | public void setMessage(String message) { 18 | this.message = message; 19 | } 20 | 21 | public String getHeadMember() { 22 | if(size() > 0 && get(0) instanceof MemberList) { 23 | MemberList memberList = (MemberList)get(0); 24 | String memberListString = ""; 25 | for(Member member : memberList) { 26 | memberListString += member.getName() + " "; 27 | } 28 | return memberListString; 29 | } 30 | return "clear"; 31 | } 32 | 33 | public int getMemberCount() { 34 | int memberCount = 0; 35 | for(PushToken token : this) { 36 | if(!(token instanceof MemberList)) { 37 | continue; 38 | } 39 | for(Member member : (MemberList)token) { 40 | ++memberCount; 41 | } 42 | } 43 | return memberCount; 44 | } 45 | 46 | public Member getDriver() { 47 | PushToken head = get(0); 48 | if(head instanceof MemberList) { 49 | return ((MemberList)head).getDriver(); 50 | } 51 | return null; 52 | } 53 | 54 | /** 55 | * @return 56 | * True if the head of the queue is a 57 | * member list and everyone in it is 58 | * ready to continue on 59 | */ 60 | public Boolean isHeadReady() { 61 | PushToken head = get(0); 62 | if(head instanceof MemberList) { 63 | return ((MemberList)head).isEveryoneReady(); 64 | } 65 | return false; 66 | } 67 | 68 | /** 69 | * @return 70 | * The state of the head (princess, prod, etc.) if 71 | * the head is a member list, else null 72 | */ 73 | public String getHeadState() { 74 | PushToken head = get(0); 75 | if(head instanceof MemberList) { 76 | return ((MemberList)head).getState(); 77 | } 78 | return null; 79 | } 80 | 81 | public Boolean isQuietPush() { 82 | PushToken head = get(0); 83 | if(head instanceof MemberList) { 84 | Member driver = ((MemberList)head).getDriver(); 85 | return driver.getConfig().isQuietDrive(); 86 | } 87 | return false; 88 | } 89 | 90 | public Boolean isHold() { 91 | PushToken head = get(0); 92 | return (head instanceof Hold); 93 | } 94 | 95 | public void onNewHead(PushBot pushBot, String channel, String sender) { 96 | PushToken head = get(0); 97 | if(head != null && head instanceof MemberList) { 98 | pushBot.sendMessage(channel, getHeadMember() + ": You're up"); 99 | if(isQuietPush()) { 100 | pushBot.sendMessage(channel, getDriver().getName() 101 | + " has asked me to be quiet for this push"); 102 | } 103 | 104 | for(Member member : ((MemberList)head)) { 105 | member.onHeadOfQueue(pushBot, this, channel); 106 | } 107 | } 108 | } 109 | 110 | @Override 111 | public String toString() { 112 | String s = ""; 113 | if(size() < 1) { 114 | s = "clear"; 115 | } 116 | else { 117 | s = Joiner.on(" | ").join(this); 118 | } 119 | if(this.message != null) { 120 | s += " !! " + this.message; 121 | } 122 | return s; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/PushTrainReader.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import org.antlr.runtime.CommonTokenStream; 5 | import org.antlr.runtime.ANTLRInputStream; 6 | 7 | public class PushTrainReader { 8 | 9 | private PushTrainReader() {} 10 | 11 | public static PushTrain parse(String pushTrainString) { 12 | try { 13 | ANTLRInputStream input = 14 | new ANTLRInputStream(new ByteArrayInputStream(pushTrainString.getBytes("UTF-8"))); 15 | 16 | PushTrainLexer pushTrainLexer = 17 | new PushTrainLexer(input); 18 | 19 | CommonTokenStream tokens = new CommonTokenStream(pushTrainLexer); 20 | 21 | PushTrainParser pushTrainParser = 22 | new PushTrainParser(tokens); 23 | 24 | return pushTrainParser.push_train(); 25 | 26 | } catch(Exception e) { 27 | // hmm 28 | } 29 | 30 | return null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/AnnounceCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | 7 | public class AnnounceCommand 8 | extends TrainCommand { 9 | 10 | private String message; 11 | private String channel; 12 | 13 | public AnnounceCommand(String message, String channel) { 14 | this.message = message; 15 | this.channel = channel; 16 | } 17 | 18 | public void onCommand(PushBot pushBot, 19 | PushTrain pushTrain, 20 | String channel, 21 | String sender) { 22 | if(!channel.equals(this.channel)) { 23 | return; 24 | } 25 | 26 | for(PushToken token : pushTrain) { 27 | if(token == null || !(token instanceof MemberList)) { 28 | continue; 29 | } 30 | String memberNames = ""; 31 | for(Member member : ((MemberList)token)) { 32 | memberNames += member.getName() + " "; 33 | } 34 | if(null != this.message && ((MemberList)token).size() > 1) { 35 | pushBot.sendMessage(channel, memberNames + ": " + this.message); 36 | } 37 | return; 38 | } 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | String s = "message " + message; 44 | return s; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/AtCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | 7 | public class AtCommand 8 | extends TrainCommand { 9 | 10 | private String state; 11 | private String channel; 12 | 13 | public AtCommand(String state, String channel) { 14 | this.state = state; 15 | this.channel = channel; 16 | } 17 | 18 | public void onCommand(PushBot pushBot, 19 | PushTrain pushTrain, 20 | String channel, 21 | String sender) { 22 | 23 | if(this.channel != null && !channel.equals(this.channel)) { 24 | return; 25 | } 26 | 27 | if(pushTrain.isQuietPush()) { 28 | return; 29 | } 30 | 31 | for(PushToken token : pushTrain) { 32 | if(token == null || !(token instanceof MemberList)) { 33 | continue; 34 | } 35 | ((MemberList)token).setState(this.state); 36 | String memberNames = ""; 37 | for(Member member : ((MemberList)token)) { 38 | member.setStatus(null); 39 | memberNames += member.getName() + " "; 40 | } 41 | return; 42 | } 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | String s = "at " + state; 48 | return s; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/ConfigCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | 5 | import java.net.InetAddress; 6 | 7 | public class ConfigCommand 8 | extends TrainCommand { 9 | 10 | public ConfigCommand() {} 11 | 12 | public void onCommand(PushBot pushBot, 13 | PushTrain pushTrain, 14 | String channel, 15 | String sender) { 16 | 17 | try { 18 | InetAddress address = InetAddress.getLocalHost(); 19 | String hostName = address.getHostName(); 20 | String url = "http://" + hostName + ":8080/#/" + sender; 21 | pushBot.sendMessage(channel, url); 22 | } 23 | catch(Throwable t) { 24 | t.printStackTrace(); 25 | } 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | return "config"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/DoneCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.PushToken; 5 | import com.etsy.pushbot.tokens.MemberList; 6 | 7 | public class DoneCommand extends TrainCommand { 8 | 9 | public DoneCommand() {} 10 | 11 | public void onCommand(PushBot pushBot, 12 | PushTrain pushTrain, 13 | String channel, 14 | String sender) { 15 | int i = 0; 16 | for(PushToken token : pushTrain) { 17 | if(token != null && token instanceof MemberList) { 18 | pushTrain.remove(i); 19 | return; 20 | } 21 | ++i; 22 | } 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | String s = "done"; 28 | return s; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/DriveCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | import java.util.*; 7 | 8 | public class DriveCommand 9 | extends TrainCommand { 10 | 11 | public DriveCommand() {} 12 | 13 | public void onCommand(PushBot pushBot, 14 | PushTrain pushTrain, 15 | String channel, 16 | String sender) { 17 | 18 | PushTrain tokenToDrive = new PushTrain(); 19 | Map tokenMember = new HashMap(); 20 | outer: for(PushToken token : pushTrain) { 21 | if(token != null && token instanceof MemberList) { 22 | for(Member member : (MemberList)token) { 23 | if(member.getName().equals(sender)) { 24 | tokenMember.put(token, member); 25 | break outer; 26 | } 27 | } 28 | } 29 | } 30 | for (Map.Entry e : tokenMember.entrySet()) { 31 | PushToken token = e.getKey(); 32 | Member member = e.getValue(); 33 | ((MemberList)token).remove(member); 34 | ((MemberList)token).addFirst(member); 35 | } 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | String s = "drive"; 41 | return s; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/GoodCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | 7 | public class GoodCommand 8 | extends TrainCommand { 9 | 10 | public GoodCommand() {} 11 | 12 | public void onCommand(PushBot pushBot, 13 | PushTrain pushTrain, 14 | String channel, 15 | String sender) { 16 | 17 | if(pushTrain.isQuietPush()) { 18 | return; 19 | } 20 | 21 | for(PushToken token : pushTrain) { 22 | if(token == null || !(token instanceof MemberList)) { 23 | continue; 24 | } 25 | for(Member member : ((MemberList)token)) { 26 | if(sender.equals(member.getName())) { 27 | member.setStatus("*"); 28 | if(((MemberList)token).isEveryoneReady()) { 29 | pushBot.sendMessage(channel, pushTrain.getHeadMember() + ": everyone is ready"); 30 | } 31 | return; 32 | } 33 | } 34 | } 35 | } 36 | 37 | public boolean isEveryoneReady(MemberList memberList) { 38 | for(Member member : memberList) { 39 | if(!"*".equals(member.getStatus()) && !member.isDark()) { 40 | return false; 41 | } 42 | } 43 | return true; 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return "uhoh"; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/HelpCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | 5 | public class HelpCommand 6 | extends TrainCommand { 7 | 8 | public HelpCommand() {} 9 | 10 | public void onCommand(PushBot pushBot, 11 | PushTrain pushTrain, 12 | String channel, 13 | String sender) { 14 | pushBot.sendMessage(channel, " See http://github.com/Etsy/PushBot#readme"); 15 | } 16 | 17 | @Override 18 | public String toString() { 19 | return "help"; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/HoldCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.Hold; 5 | import com.etsy.pushbot.tokens.MemberList; 6 | import com.etsy.pushbot.tokens.PushToken; 7 | 8 | public class HoldCommand 9 | extends TrainCommand { 10 | 11 | private String message; 12 | 13 | public HoldCommand() {} 14 | 15 | public void setMessage(String message) { 16 | this.message = message; 17 | } 18 | 19 | public void onCommand(PushBot pushBot, 20 | PushTrain pushTrain, 21 | String channel, 22 | String sender) { 23 | Hold hold = new Hold(); 24 | hold.setMessage(this.message); 25 | pushTrain.addFirst(hold); 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | String s = "hold"; 31 | if(message != null) { 32 | s += " " + message; 33 | } 34 | return s; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/JoinCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | 7 | public class JoinCommand 8 | extends TrainCommand { 9 | 10 | private String type = null; 11 | private String with = null; 12 | private String before = null; 13 | private boolean last = false; 14 | private String message = null; 15 | 16 | public JoinCommand() {} 17 | 18 | public void setType(String type) { 19 | if("hold".equals(type)) { 20 | type = "HOLD"; 21 | } 22 | this.type = type; 23 | } 24 | 25 | public void setLast(boolean last) { 26 | this.last = last; 27 | } 28 | 29 | public void setWith(String member) { 30 | this.with = member; 31 | } 32 | 33 | public void setBefore(String member) { 34 | this.before = member; 35 | } 36 | 37 | public void setMessage(String message) { 38 | this.message = message; 39 | } 40 | 41 | public void onCommand(PushBot pushBot, 42 | PushTrain pushTrain, 43 | String channel, 44 | String sender) { 45 | 46 | Member member = new Member(sender); 47 | member.setType(this.type); 48 | 49 | if(this.with != null) { 50 | this.joinWith(pushTrain, member); 51 | return; 52 | } 53 | 54 | if(this.before != null) { 55 | this.joinBefore(pushTrain, member); 56 | return; 57 | } 58 | 59 | if(this.last || (this.type != null && !member.isDark())) { 60 | this.joinLast(pushTrain, member); 61 | return; 62 | } 63 | 64 | this.joinWhereConvenient(pushTrain, member); 65 | } 66 | 67 | protected void joinLast(PushTrain pushTrain, 68 | Member member) { 69 | MemberList memberList = new MemberList(); 70 | this.joinMemberList(memberList, member); 71 | pushTrain.add(memberList); 72 | return; 73 | } 74 | 75 | protected void joinWith(PushTrain pushTrain, 76 | Member member) { 77 | if(this.with == null) { 78 | return; 79 | } 80 | for(PushToken token : pushTrain) { 81 | if(token == null || !(token instanceof MemberList)) { 82 | continue; 83 | } 84 | for(Member queuedMember : (MemberList)token) { 85 | if(this.with.toLowerCase().equals(queuedMember.getName().toLowerCase())) { 86 | this.joinMemberList((MemberList)token, member); 87 | return; 88 | } 89 | } 90 | } 91 | } 92 | 93 | protected void joinBefore(PushTrain pushTrain, 94 | Member member) { 95 | int i = -1; 96 | for(PushToken token : pushTrain) { 97 | ++i; 98 | if(token == null || !(token instanceof MemberList)) { 99 | continue; 100 | } 101 | for(Member queuedMember : (MemberList)token) { 102 | if(!queuedMember.getName().equals(this.before)) { 103 | continue; 104 | } 105 | MemberList memberList = new MemberList(); 106 | this.joinMemberList(memberList, member); 107 | pushTrain.add(i, memberList); 108 | return; 109 | } 110 | } 111 | } 112 | 113 | protected void joinWhereConvenient(PushTrain pushTrain, 114 | Member member) { 115 | boolean first = true; 116 | for(PushToken token : pushTrain) { 117 | if(!first && token != null && token instanceof MemberList) { 118 | boolean isOpen = true; 119 | for(Member queuedMember : (MemberList)token) { 120 | if(queuedMember.isConfig() || (member.getType() == null && queuedMember.getType() != null && !queuedMember.isDark())) { 121 | isOpen = false; 122 | break; 123 | } 124 | } 125 | if(((MemberList)token).size() < 5 && isOpen) { 126 | this.joinMemberList((MemberList)token, member); 127 | return; 128 | } 129 | } 130 | if(token instanceof MemberList) { 131 | first = false; 132 | } 133 | } 134 | 135 | this.joinLast(pushTrain, member); 136 | } 137 | 138 | /** 139 | * Add a member to a member list and possibly 140 | * set a message on that memberlist 141 | */ 142 | private void joinMemberList(MemberList memberList, Member member) { 143 | memberList.add(member); 144 | 145 | if(this.message != null) { 146 | memberList.setMessage(this.message); 147 | } 148 | } 149 | 150 | @Override 151 | public String toString() { 152 | String s = "join"; 153 | if(type != null) { 154 | s += " " + type; 155 | } 156 | if(with != null) { 157 | s += " with " + with; 158 | } 159 | 160 | if(message != null) { 161 | s += " \"" + message + "\""; 162 | } 163 | 164 | return s; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/MessageCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | 5 | public class MessageCommand 6 | extends TrainCommand { 7 | 8 | private String message; 9 | 10 | public MessageCommand(String message) { 11 | this.message = message; 12 | } 13 | 14 | public void onCommand(PushBot pushBot, 15 | PushTrain pushTrain, 16 | String channel, 17 | String sender) { 18 | if(this.message.equals("-")) { 19 | this.message = null; 20 | } 21 | pushTrain.setMessage(this.message); 22 | } 23 | 24 | @Override 25 | public String toString() { 26 | return "help"; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/NevermindCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | 7 | public class NevermindCommand 8 | extends TrainCommand { 9 | 10 | private String member = null; 11 | 12 | public NevermindCommand() {} 13 | 14 | public void setMember(String member) { 15 | this.member = member; 16 | } 17 | 18 | public void onCommand(PushBot pushBot, 19 | PushTrain pushTrain, 20 | String channel, 21 | String sender) { 22 | if(this.member == null) { 23 | this.member = sender; 24 | } 25 | 26 | PushTrain tokenToRemove = new PushTrain(); 27 | for(PushToken token : pushTrain) { 28 | if(token != null && token instanceof MemberList) { 29 | MemberList toRemove = new MemberList(); 30 | for(Member member : (MemberList)token) { 31 | if(member.getName().equals(this.member)) { 32 | toRemove.add(member); 33 | } 34 | } 35 | for(Member member : toRemove) { 36 | ((MemberList)token).remove(member); 37 | } 38 | if(((MemberList)token).size() < 1) { 39 | tokenToRemove.add(token); 40 | } 41 | } 42 | } 43 | for(PushToken token : tokenToRemove) { 44 | pushTrain.remove(token); 45 | } 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | String s = "unshift"; 51 | return s; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/PopCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | 7 | public class PopCommand 8 | extends TrainCommand { 9 | 10 | private String member = null; 11 | 12 | public PopCommand() {} 13 | 14 | public void setMember(String member) { 15 | this.member = member; 16 | } 17 | 18 | public void onCommand(PushBot pushBot, 19 | PushTrain pushTrain, 20 | String channel, 21 | String sender 22 | ) { 23 | 24 | if(this.member == null) { 25 | this.member = sender; 26 | } 27 | 28 | Member targetMember = null; 29 | PushToken targetToken = null; 30 | for(PushToken token : pushTrain) { 31 | if(token != null && token instanceof MemberList) { 32 | for(Member member : (MemberList)token) { 33 | if(member.getName().equals(this.member)) { 34 | // Maintain ref to last instance of member in train 35 | targetMember = member; 36 | targetToken = token; 37 | } 38 | } 39 | } 40 | } 41 | 42 | if (targetMember != null) { 43 | ((MemberList)targetToken).remove(targetMember); 44 | if (((MemberList)targetToken).size() < 1) { 45 | pushTrain.remove(targetToken); 46 | } 47 | } 48 | 49 | } 50 | 51 | @Override 52 | public String toString() { 53 | String s = "pop"; 54 | return s; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/SayCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | 5 | public class SayCommand 6 | extends TrainCommand { 7 | 8 | private String where; 9 | private String what; 10 | 11 | public void setMessage(String message) { 12 | String[] whereWhat = message.replaceAll("\"$|^\"", "").trim().split(" ", 2); 13 | if (whereWhat.length == 2) { 14 | this.where = whereWhat[0].trim(); 15 | this.what = whereWhat[1].trim(); 16 | } 17 | } 18 | 19 | public SayCommand() { 20 | } 21 | 22 | public void onCommand(PushBot pushBot, 23 | PushTrain pushTrain, 24 | String channel, 25 | String sender) { 26 | if (this.where == null) { 27 | return; 28 | } 29 | pushBot.sendMessage(this.where, this.what); 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return "say"; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/TrainCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | 5 | abstract public class TrainCommand { 6 | 7 | abstract public void onCommand(PushBot pushBot, 8 | PushTrain pushTrain, 9 | String channel, 10 | String sender); 11 | 12 | abstract public String toString(); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/UhohCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.MemberList; 5 | import com.etsy.pushbot.tokens.PushToken; 6 | 7 | public class UhohCommand 8 | extends TrainCommand { 9 | 10 | public UhohCommand() {} 11 | 12 | public void onCommand(PushBot pushBot, 13 | PushTrain pushTrain, 14 | String channel, 15 | String sender) { 16 | for(PushToken token : pushTrain) { 17 | if(token == null || !(token instanceof MemberList)) { 18 | continue; 19 | } 20 | for(Member member : ((MemberList)token)) { 21 | if(sender.equals(member.getName())) { 22 | member.setStatus("!"); 23 | return; 24 | } 25 | } 26 | } 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return "uhoh"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/command/UnholdCommand.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.command; 2 | 3 | import com.etsy.pushbot.*; 4 | import com.etsy.pushbot.tokens.Hold; 5 | import com.etsy.pushbot.tokens.MemberList; 6 | import com.etsy.pushbot.tokens.PushToken; 7 | 8 | public class UnholdCommand 9 | extends TrainCommand { 10 | 11 | public UnholdCommand() {} 12 | 13 | public void onCommand(PushBot pushBot, 14 | PushTrain pushTrain, 15 | String channel, 16 | String sender) { 17 | if(pushTrain == null || pushTrain.size() < 1) { 18 | System.out.println("empty"); 19 | return; 20 | } 21 | PushToken pushToken = pushTrain.get(0); 22 | if(pushToken != null && pushToken instanceof Hold) { 23 | pushTrain.removeFirst(); 24 | } 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "unhold"; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/config/Config.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.config; 2 | 3 | import java.io.IOException; 4 | import java.util.Collection; 5 | import org.codehaus.jackson.map.ObjectMapper; 6 | 7 | public class Config { 8 | 9 | private static final ObjectMapper mapper 10 | = new ObjectMapper(); 11 | 12 | public Boolean quietDrive = false; 13 | public Boolean sendNotifoWhenUp = false; 14 | public String notifoUsername = ""; 15 | public String notifoApiSecret = ""; 16 | 17 | public Config() {} 18 | 19 | public Boolean isQuietDrive() { return quietDrive; } 20 | 21 | public String toString() 22 | { 23 | try { 24 | return mapper.writeValueAsString(this); 25 | } catch (IOException e) { 26 | System.err.println("IO errors are not possible here"); 27 | System.err.println(e.getMessage()); 28 | } 29 | 30 | return ""; 31 | } 32 | 33 | public static Config fromString(String json) { 34 | try { 35 | return mapper.readValue(json, Config.class); 36 | } catch (IOException e) { 37 | System.err.println(json); 38 | e.printStackTrace(); 39 | } 40 | return null; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/config/ConfigDao.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.config; 2 | 3 | import com.etsy.pushbot.config.Config; 4 | import java.util.Map; 5 | import java.util.HashMap; 6 | import java.sql.*; 7 | 8 | public class ConfigDao { 9 | 10 | private static ConfigDao instance = null; 11 | 12 | private Connection connection = null; 13 | private Map configCache = new HashMap(); 14 | 15 | public static ConfigDao getInstance() throws SQLException { 16 | if(ConfigDao.instance == null) { 17 | ConfigDao.instance = new ConfigDao(); 18 | } 19 | return ConfigDao.instance; 20 | } 21 | 22 | private ConfigDao() throws SQLException { 23 | this.connection = getConnection(); 24 | PreparedStatement preparedStatement = 25 | this.connection.prepareStatement("SELECT * FROM config;"); 26 | ResultSet rs = preparedStatement.executeQuery(); 27 | while (rs.next()) { 28 | System.out.println(rs.getString("member") + "\t" + rs.getString("config")); 29 | } 30 | rs.close(); 31 | } 32 | 33 | public Config getConfigForMember(String member) throws SQLException { 34 | 35 | Config cachedConfig = this.configCache.get(member); 36 | if(cachedConfig != null) { 37 | return cachedConfig; 38 | } 39 | 40 | PreparedStatement preparedStatement = 41 | connection.prepareStatement("SELECT config FROM config WHERE member=?;"); 42 | 43 | preparedStatement.setString(1, member); 44 | 45 | ResultSet rs = preparedStatement.executeQuery(); 46 | 47 | if(!rs.next()) { 48 | this.configCache.put(member, new Config()); 49 | } 50 | else { 51 | this.configCache.put(member,Config.fromString(rs.getString("config"))); 52 | } 53 | rs.close(); 54 | 55 | return this.configCache.get(member); 56 | } 57 | 58 | public void setConfigForMember(String member, Config config) throws SQLException { 59 | PreparedStatement preparedStatement = 60 | this.connection.prepareStatement("REPLACE INTO config VALUES (?, ?);"); 61 | 62 | preparedStatement.setString(1, member); 63 | preparedStatement.setString(2, config.toString()); 64 | preparedStatement.executeUpdate(); 65 | 66 | this.configCache.put(member, config); 67 | 68 | System.out.println("added config for " + member + ": " + config.toString()); 69 | } 70 | 71 | private Connection getConnection() throws SQLException { 72 | try { 73 | Class.forName("org.sqlite.JDBC"); 74 | } 75 | catch(ClassNotFoundException e) { 76 | e.printStackTrace(); 77 | return null; 78 | } 79 | 80 | Connection connection = 81 | DriverManager.getConnection("jdbc:sqlite:pushbot.db"); 82 | 83 | Statement statement = connection.createStatement(); 84 | statement.executeUpdate("CREATE TABLE IF NOT EXISTS config (member text, config text, PRIMARY KEY (member)); "); 85 | 86 | return connection; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/config/ConfigServer.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.config; 2 | 3 | import java.io.File; 4 | import java.net.URL; 5 | 6 | import com.etsy.pushbot.PushBot; 7 | import org.eclipse.jetty.server.Server; 8 | import org.eclipse.jetty.server.handler.*; 9 | import org.eclipse.jetty.webapp.WebAppContext; 10 | import org.eclipse.jetty.servlet.ServletHolder; 11 | 12 | public class ConfigServer { 13 | 14 | /** 15 | * @param args 16 | */ 17 | public ConfigServer(PushBot pushBot) throws Exception { 18 | 19 | String webappDirLocation = "src/main/webapp/"; 20 | 21 | //The port that we should run on can be set into an environment variable 22 | //Look for that variable and default to 8080 if it isn't there. 23 | String webPort = System.getenv("PORT"); 24 | if(webPort == null || webPort.isEmpty()) { 25 | webPort = "8080"; 26 | } 27 | 28 | Server server = new Server(Integer.valueOf(webPort)); 29 | WebAppContext root = new WebAppContext(); 30 | 31 | root.setContextPath("/"); 32 | root.setDescriptor(webappDirLocation+"/WEB-INF/web.xml"); 33 | root.setResourceBase(webappDirLocation); 34 | 35 | // Add the status servlet 36 | ServletHolder servletHolder = new ServletHolder(new StatusServlet(pushBot)); 37 | root.addServlet(servletHolder, "/status"); 38 | 39 | //Parent loader priority is a class loader setting that Jetty accepts. 40 | //By default Jetty will behave like most web containers in that it will 41 | //allow your application to replace non-server libraries that are part of the 42 | //container. Setting parent loader priority to true changes this behavior. 43 | //Read more here: http://wiki.eclipse.org/Jetty/Reference/Jetty_Classloading 44 | root.setParentLoaderPriority(true); 45 | 46 | server.setHandler(root); 47 | 48 | server.start(); 49 | server.join(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/config/Response.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.config; 2 | 3 | import java.io.IOException; 4 | import java.util.Collection; 5 | import org.codehaus.jackson.map.ObjectMapper; 6 | 7 | public class Response 8 | { 9 | private static final ObjectMapper mapper 10 | = new ObjectMapper(); 11 | 12 | private int errorId = 0; 13 | private String errorMessage = null; 14 | private T response = null; 15 | 16 | public Response(T response) 17 | { 18 | this.response = response; 19 | } 20 | 21 | public Response(int errorId, String errorMessage) 22 | { 23 | this.errorId = errorId; 24 | this.errorMessage = errorMessage; 25 | } 26 | 27 | public Response(T response, 28 | int errorId, String errorMessage) 29 | { 30 | this.errorId = errorId; 31 | this.errorMessage = errorMessage; 32 | this.response = response; 33 | } 34 | 35 | public int getErrorId() { return errorId; } 36 | public String getErrorMessage() { return errorMessage; } 37 | public T getResponse() { return response; } 38 | 39 | public int getCount() { 40 | if(response instanceof Collection) 41 | return ((Collection)response).size(); 42 | if(response == null) 43 | return 0; 44 | return 1; 45 | } 46 | 47 | public String toString() 48 | { 49 | try { 50 | return mapper.writeValueAsString(this); 51 | } catch (IOException e) { 52 | System.err.println("IO errors are not possible here"); 53 | System.err.println(e.getMessage()); 54 | } 55 | 56 | return ""; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/config/Status.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.config; 2 | 3 | import java.io.IOException; 4 | import java.util.Collection; 5 | import org.codehaus.jackson.map.ObjectMapper; 6 | 7 | public class Status { 8 | 9 | private static final ObjectMapper mapper 10 | = new ObjectMapper(); 11 | 12 | public Boolean isHold = false; 13 | public Boolean isEveryoneReady = false; 14 | public String driver = ""; 15 | public String head = ""; 16 | public Integer memberCount = 0; 17 | public String headState = null; 18 | 19 | public Status() {} 20 | 21 | public String toString() 22 | { 23 | try { 24 | return mapper.writeValueAsString(this); 25 | } catch (IOException e) { 26 | System.err.println("IO errors are not possible here"); 27 | System.err.println(e.getMessage()); 28 | } 29 | return ""; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/config/StatusServlet.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.config; 2 | 3 | import com.etsy.pushbot.PushBot; 4 | import com.etsy.pushbot.config.Response; 5 | 6 | import java.io.IOException; 7 | import java.io.PrintWriter; 8 | import javax.servlet.http.HttpServlet; 9 | import javax.servlet.http.HttpServletRequest; 10 | import javax.servlet.http.HttpServletResponse; 11 | import javax.servlet.ServletException; 12 | import org.eclipse.jetty.servlet.ServletHolder; 13 | 14 | /** 15 | * 16 | */ 17 | public class StatusServlet extends HttpServlet { 18 | private PushBot pushbot; 19 | 20 | public StatusServlet(PushBot pushbot) { 21 | this.pushbot = pushbot; 22 | } 23 | 24 | /** 25 | * 26 | */ 27 | @Override 28 | protected void doGet(HttpServletRequest request, 29 | HttpServletResponse response) 30 | throws ServletException, IOException { 31 | 32 | String channel = "#" + request.getParameter("channel"); 33 | String callback = request.getParameter("callback"); 34 | 35 | response.setContentType("text/javascript"); 36 | response.setHeader("Cache-Control", "no-cache"); 37 | PrintWriter writer = response.getWriter(); 38 | 39 | try { 40 | writer.print(callback + "(" 41 | + new Response(this.pushbot.getStatus(channel)).toString() 42 | + ")"); 43 | } 44 | catch(Exception exception) { 45 | writer.print(callback + "(" 46 | + new Response(1, exception.getMessage()).toString() 47 | + ")" ); 48 | System.err.println(exception.getMessage()); 49 | exception.printStackTrace(); 50 | } 51 | writer.flush(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/config/api/GetMemberConfig.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.config.api; 2 | 3 | import com.etsy.pushbot.config.ConfigDao; 4 | import com.etsy.pushbot.config.Config; 5 | import com.etsy.pushbot.config.Response; 6 | import javax.ws.rs.GET; 7 | import javax.ws.rs.POST; 8 | import javax.ws.rs.Path; 9 | import javax.ws.rs.Produces; 10 | import javax.ws.rs.Consumes; 11 | import javax.ws.rs.PathParam; 12 | import javax.ws.rs.core.Context; 13 | import javax.ws.rs.core.MediaType; 14 | 15 | 16 | @Path("/member/{member}") 17 | public class GetMemberConfig { 18 | 19 | @GET 20 | @Produces(MediaType.APPLICATION_JSON) 21 | public String get(@PathParam("member") String member) { 22 | try { 23 | Config config = ConfigDao.getInstance().getConfigForMember(member); 24 | return new Response(config).toString(); 25 | } 26 | catch(Throwable t) { 27 | t.printStackTrace(); 28 | return new Response(1, t.getMessage()).toString(); 29 | } 30 | } 31 | 32 | @POST 33 | @Produces(MediaType.APPLICATION_JSON) 34 | @Consumes(MediaType.APPLICATION_JSON) 35 | public String set(@PathParam("member") String member, String configJson) { 36 | Config config = Config.fromString(configJson); 37 | 38 | try { 39 | ConfigDao.getInstance().setConfigForMember(member, config); 40 | return new Response(0,"success").toString(); 41 | } 42 | catch(Throwable t) { 43 | t.printStackTrace(); 44 | return new Response(1, t.getMessage()).toString(); 45 | } 46 | } 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/tokens/Hold.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.tokens; 2 | 3 | import com.etsy.pushbot.*; 4 | 5 | public class Hold implements PushToken { 6 | 7 | private String message; 8 | 9 | public Hold() {} 10 | 11 | public void setMessage(String message) { 12 | if("".equals(message)) { 13 | return; 14 | } 15 | this.message = message; 16 | } 17 | 18 | @Override 19 | public String toString() { 20 | String s ="HOLD"; 21 | if(message != null) { 22 | s += " " + message; 23 | } 24 | return s; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/tokens/MemberList.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.tokens; 2 | 3 | import com.etsy.pushbot.*; 4 | import java.util.LinkedList; 5 | import com.google.common.base.Joiner; 6 | 7 | public class MemberList extends LinkedList implements PushToken { 8 | 9 | String state = null; 10 | String message = null; 11 | 12 | public MemberList() { 13 | super(); 14 | } 15 | 16 | public void setState(String state) { 17 | this.state = state; 18 | } 19 | 20 | public void setMessage(String message) { 21 | this.message = message; 22 | } 23 | 24 | public String getState() { 25 | return this.state; 26 | } 27 | 28 | public Member getDriver() { 29 | return get(0); 30 | } 31 | 32 | public String getMemberList() { 33 | String members = ""; 34 | for(Member member : this) { 35 | members += member.getName() + " "; 36 | } 37 | return members; 38 | } 39 | 40 | /** 41 | * @return 42 | * True if everyone is ready, else false 43 | */ 44 | public boolean isEveryoneReady() { 45 | for(Member member : this) { 46 | if(!"*".equals(member.getStatus()) && !member.isDark()) { 47 | return false; 48 | } 49 | } 50 | return true; 51 | } 52 | 53 | @Override 54 | public String toString() { 55 | String s = ""; 56 | 57 | if(this.state != null) { 58 | s += "<" + this.state + "> "; 59 | } 60 | 61 | s += Joiner.on(" + ").join(this); 62 | 63 | if(this.message != null) { 64 | s += " " + this.message; 65 | } 66 | 67 | return s; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/etsy/pushbot/tokens/PushToken.java: -------------------------------------------------------------------------------- 1 | package com.etsy.pushbot.tokens; 2 | 3 | abstract public interface PushToken { 4 | abstract public String toString(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/lib/jersey-core-1.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etsy/PushBot/17e1b3070d395e3644037fc9091ec4d3195dd4f4/src/main/webapp/WEB-INF/lib/jersey-core-1.6.jar -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/lib/jersey-json-1.6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/etsy/PushBot/17e1b3070d395e3644037fc9091ec4d3195dd4f4/src/main/webapp/WEB-INF/lib/jersey-json-1.6.jar -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | PushBotConfig 9 | com.sun.jersey.spi.container.servlet.ServletContainer 10 | 11 | com.sun.jersey.config.property.packages 12 | com.etsy.pushbot.config.api 13 | 14 | 20 | 1 21 | 22 | 23 | PushBotConfig 24 | /api/* 25 | 26 | 27 | index.html 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/main/webapp/css/buttons.less: -------------------------------------------------------------------------------- 1 | .button 2 | { 3 | .roundedCorners(4px); 4 | padding: 2px 10px 2px 10px; 5 | white-space: nowrap; 6 | display: inline-block; 7 | cursor: pointer; 8 | 9 | .gradientBackgroundTop(#fff,#ccc); 10 | .boxShadow(1px, 1px, 1px, #eee); 11 | border: 1px solid #eee; 12 | color: #666; 13 | text-shadow: #fff 1px 1px 1px; 14 | border-bottom: 1px solid #eee; 15 | 16 | font-family: helvetica, arial, sans-serif; 17 | font-weight: 500; 18 | font-size: 12pt; 19 | letter-spacing: 0.06em; 20 | text-transform: capitalize; 21 | word-spacing: -1px; 22 | text-decoration: none; 23 | } 24 | 25 | .button::-moz-selection, 26 | .button::-webkit-selection, 27 | .button::selection { 28 | background-color: transparent; 29 | } 30 | 31 | .disabled 32 | { 33 | cursor: inherit !important; 34 | opacity: 0.5; 35 | } 36 | 37 | .button:not(.disabled):hover 38 | { 39 | color: #333; 40 | } 41 | 42 | .button:not(.disabled):hover:active 43 | { 44 | .gradientBackground(#fff,#ddd); 45 | .boxShadow(0,0,0,#fff); 46 | } 47 | -------------------------------------------------------------------------------- /src/main/webapp/css/definitions.less: -------------------------------------------------------------------------------- 1 | .gradientBackground(@from: #aaa, @to: #777, @stopColor: #fff, @stopPosition: 1.0) 2 | { 3 | background: @stopColor; 4 | background: -webkit-gradient(linear, left bottom, left top, from(@from), to(@to), color-stop(@stopPosition, @stopColor)); 5 | background: -moz-linear-gradient(bottom, @from, @to); 6 | } 7 | 8 | .gradientBackgroundTop(@from: #aaa, @to: #777, @stopColor: #fff, @stopPosition: 1.0) 9 | { 10 | background: @stopColor; 11 | background: -webkit-gradient(linear, left top, left bottom, from(@from), to(@to), color-stop(@stopPosition, @stopColor)); 12 | background: -moz-linear-gradient(top, @from, @to); 13 | } 14 | 15 | .roundedCorners (@radius: 5px) { 16 | -moz-border-radius: @radius; 17 | -webkit-border-radius: @radius; 18 | border-radius: @radius; 19 | 20 | } 21 | 22 | .boxShadow(@horizontal: 1px, @vertical: 1px, @blur: 6px, @color: #ddd) 23 | { 24 | -moz-box-shadow: @horizontal @vertical @blur @color; 25 | -webkit-box-shadow: @horizontal @vertical @blur @color; 26 | box-shadow: @horizontal @vertical @blur @color; 27 | } 28 | 29 | .rotate(@angle: 45deg) 30 | { 31 | -moz-transform:rotate(@angle); 32 | -webkit-transform:rotate(@angle); 33 | -o-transform:rotate(@angle); 34 | } 35 | 36 | .fBubble(@border: 1px solid #aaa, @background-color: #f0f0f0) 37 | { 38 | border: @border; 39 | background-color: @background-color; 40 | display: inline-block; 41 | padding: 10px 10px 10px 10px; 42 | .roundedCorners(8px); 43 | position: relative; 44 | .boxShadow(); 45 | .arrow { 46 | border-left: @border; 47 | border-bottom: @border; 48 | background-color: @background-color; 49 | display: inline-block; 50 | width: 15px; 51 | height: 15px; 52 | .rotate(135deg); 53 | position: absolute; 54 | left: 100px; 55 | top: -9px; 56 | } 57 | } 58 | 59 | .fTransition(@definition: color 1s ease-in) 60 | { 61 | -webkit-transition:@definition; 62 | -moz-transition:@definition; 63 | -o-transition:@definition; 64 | transition:@definition; 65 | } 66 | 67 | 68 | .fixedWidthNoWrap(@width: 100%, @minWidth: 100%) 69 | { 70 | min-width: @minWidth; 71 | width: @width; 72 | text-overflow: ellipsis; 73 | white-space: nowrap; 74 | overflow: hidden; 75 | } 76 | 77 | .columns(@count) 78 | { 79 | column-count: @count; 80 | -moz-column-count: @count; 81 | -webkit-column-count: @count; 82 | 83 | } -------------------------------------------------------------------------------- /src/main/webapp/css/style.less: -------------------------------------------------------------------------------- 1 | @import "definitions.less"; 2 | @import "buttons.less"; 3 | 4 | body { 5 | display: inherit; 6 | text-align: left; 7 | width: 500px; 8 | margin: 100px auto; 9 | } 10 | 11 | .hidden { 12 | display: none; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | color: #2A41D4; 18 | } 19 | 20 | .form 21 | { 22 | .roundedCorners(6px); 23 | background-color: #f7f7f7; 24 | width: 410px; 25 | padding: 20px 20px 20px 20px; 26 | 27 | h1 28 | { 29 | color: #666; 30 | font-size: 16pt; 31 | margin-bottom: 20px; 32 | } 33 | 34 | h2 35 | { 36 | color: #777; 37 | font-size: 14pt; 38 | margin-bottom: 15px; 39 | } 40 | 41 | fieldset 42 | { 43 | dl 44 | { 45 | margin-bottom: 20px; 46 | 47 | dt 48 | { 49 | margin-bottom: 3px; 50 | 51 | label 52 | { 53 | color: #888; 54 | font-size: 12pt; 55 | } 56 | 57 | input[type=checkbox],label { 58 | cursor: pointer 59 | } 60 | } 61 | 62 | dd 63 | { 64 | padding-bottom: 20px; 65 | 66 | input 67 | { 68 | /* structure */ 69 | .roundedCorners(3px); 70 | padding: 3px 3px 3px 3px; 71 | width: 400px; 72 | 73 | /* look+feel */ 74 | .gradientBackground(#fff,#eee); 75 | border: 1px solid #ddd; 76 | color: #555; 77 | text-shadow: #ccc 1px 1px 1px; 78 | 79 | /* typography */ 80 | font-size: 14pt; 81 | } 82 | 83 | .note 84 | { 85 | font-size: 8pt; 86 | font-weight: lighter; 87 | color: #999; 88 | padding: 0 3px 0 0px; 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | label.error 96 | { 97 | .roundedCorners(6px); 98 | border-top-left-radius: 0; 99 | border-top-right-radius: 0; 100 | margin: 0 0 0 2px; 101 | padding: 0px 5px 1px 5px; 102 | color: #555; 103 | background-color: #ffc; 104 | } 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/main/webapp/css/vendor/reset-fonts-grids.css: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010, Yahoo! Inc. All rights reserved. 3 | Code licensed under the BSD License: 4 | http://developer.yahoo.com/yui/license.html 5 | version: 2.8.1 6 | */ 7 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}body{font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;}select,input,button,textarea,button{font:99% arial,helvetica,clean,sans-serif;}table{font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;}body{text-align:center;}#doc,#doc2,#doc3,#doc4,.yui-t1,.yui-t2,.yui-t3,.yui-t4,.yui-t5,.yui-t6,.yui-t7{margin:auto;text-align:left;width:57.69em;*width:56.25em;}#doc2{width:73.076em;*width:71.25em;}#doc3{margin:auto 10px;width:auto;}#doc4{width:74.923em;*width:73.05em;}.yui-b{position:relative;}.yui-b{_position:static;}#yui-main .yui-b{position:static;}#yui-main,.yui-g .yui-u .yui-g{width:100%;}.yui-t1 #yui-main,.yui-t2 #yui-main,.yui-t3 #yui-main{float:right;margin-left:-25em;}.yui-t4 #yui-main,.yui-t5 #yui-main,.yui-t6 #yui-main{float:left;margin-right:-25em;}.yui-t1 .yui-b{float:left;width:12.30769em;*width:12.00em;}.yui-t1 #yui-main .yui-b{margin-left:13.30769em;*margin-left:13.05em;}.yui-t2 .yui-b{float:left;width:13.8461em;*width:13.50em;}.yui-t2 #yui-main .yui-b{margin-left:14.8461em;*margin-left:14.55em;}.yui-t3 .yui-b{float:left;width:23.0769em;*width:22.50em;}.yui-t3 #yui-main .yui-b{margin-left:24.0769em;*margin-left:23.62em;}.yui-t4 .yui-b{float:right;width:13.8456em;*width:13.50em;}.yui-t4 #yui-main .yui-b{margin-right:14.8456em;*margin-right:14.55em;}.yui-t5 .yui-b{float:right;width:18.4615em;*width:18.00em;}.yui-t5 #yui-main .yui-b{margin-right:19.4615em;*margin-right:19.125em;}.yui-t6 .yui-b{float:right;width:23.0769em;*width:22.50em;}.yui-t6 #yui-main .yui-b{margin-right:24.0769em;*margin-right:23.62em;}.yui-t7 #yui-main .yui-b{display:block;margin:0 0 1em 0;}#yui-main .yui-b{float:none;width:auto;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf,.yui-gc .yui-u,.yui-gd .yui-g,.yui-g .yui-gc .yui-u,.yui-ge .yui-u,.yui-ge .yui-g,.yui-gf .yui-g,.yui-gf .yui-u{float:right;}.yui-g div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first,.yui-ge div.first,.yui-gf div.first,.yui-g .yui-gc div.first,.yui-g .yui-ge div.first,.yui-gc div.first div.first{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf{width:49.1%;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{width:32%;margin-left:1.99%;}.yui-gb .yui-u{*margin-left:1.9%;*width:31.9%;}.yui-gc div.first,.yui-gd .yui-u{width:66%;}.yui-gd div.first{width:32%;}.yui-ge div.first,.yui-gf .yui-u{width:74.2%;}.yui-ge .yui-u,.yui-gf div.first{width:24%;}.yui-g .yui-gb div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first{margin-left:0;}.yui-g .yui-g .yui-u,.yui-gb .yui-g .yui-u,.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u,.yui-ge .yui-g .yui-u,.yui-gf .yui-g .yui-u{width:49%;*width:48.1%;*margin-left:0;}.yui-g .yui-g .yui-u{width:48.1%;}.yui-g .yui-gb div.first,.yui-gb .yui-gb div.first{*margin-right:0;*width:32%;_width:31.7%;}.yui-g .yui-gc div.first,.yui-gd .yui-g{width:66%;}.yui-gb .yui-g div.first{*margin-right:4%;_margin-right:1.3%;}.yui-gb .yui-gc div.first,.yui-gb .yui-gd div.first{*margin-right:0;}.yui-gb .yui-gb .yui-u,.yui-gb .yui-gc .yui-u{*margin-left:1.8%;_margin-left:4%;}.yui-g .yui-gb .yui-u{_margin-left:1.0%;}.yui-gb .yui-gd .yui-u{*width:66%;_width:61.2%;}.yui-gb .yui-gd div.first{*width:31%;_width:29.5%;}.yui-g .yui-gc .yui-u,.yui-gb .yui-gc .yui-u{width:32%;_float:right;margin-right:0;_margin-left:0;}.yui-gb .yui-gc div.first{width:66%;*float:left;*margin-left:0;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf .yui-u{margin:0;}.yui-gb .yui-gb .yui-u{_margin-left:.7%;}.yui-gb .yui-g div.first,.yui-gb .yui-gb div.first{*margin-left:0;}.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u{*width:48.1%;*margin-left:0;}.yui-gb .yui-gd div.first{width:32%;}.yui-g .yui-gd div.first{_width:29.9%;}.yui-ge .yui-g{width:24%;}.yui-gf .yui-g{width:74.2%;}.yui-gb .yui-ge div.yui-u,.yui-gb .yui-gf div.yui-u{float:right;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf div.first{float:left;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf div.first{*width:24%;_width:20%;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf .yui-u{*width:73.5%;_width:65.5%;}.yui-ge div.first .yui-gd .yui-u{width:65%;}.yui-ge div.first .yui-gd div.first{width:32%;}#hd:after,#bd:after,#ft:after,.yui-g:after,.yui-gb:after,.yui-gc:after,.yui-gd:after,.yui-ge:after,.yui-gf:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#hd,#bd,#ft,.yui-g,.yui-gb,.yui-gc,.yui-gd,.yui-ge,.yui-gf{zoom:1;} -------------------------------------------------------------------------------- /src/main/webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | PushBot Config 8 | 9 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 |
28 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/main/webapp/js/PushBotConfig.js: -------------------------------------------------------------------------------- 1 | (function($) { 2 | PushBotConfig =(function() { 3 | 4 | function _setupEventListeners() { 5 | $('#inputMember').change(_onMemberChange); 6 | $('#memberConfig input').change(_onSubmit); 7 | $.address.change(_onAddressChange); 8 | } 9 | 10 | function _onMemberChange(event) { 11 | $.address.path($('#inputMember').val()); 12 | $.address.update(); 13 | } 14 | 15 | function _onAddressChange(event) { 16 | var member = $.address.path().substr(1); 17 | if('' == member) { 18 | return; 19 | } 20 | $('#inputMember').unbind('change').val(member); 21 | $('#spanMemberName').text(member); 22 | _getConfigForMember(member); 23 | } 24 | 25 | function _getConfigForMember(memberName) { 26 | $.ajax({ 27 | url: '/api/member/'+memberName, 28 | dataType: 'json', 29 | success: _onMemberConfig 30 | }); 31 | } 32 | 33 | function _onMemberConfig(data) { 34 | $('#memberName').hide(); 35 | $('#memberConfig').show(); 36 | 37 | var response = data.response; 38 | console.log(response); 39 | 40 | if(response.quietDrive) { 41 | $('#inputQuietDrive').attr('checked', 'checked'); 42 | } 43 | if(response.sendNotifoWhenUp) { 44 | $('#inputSendNotifoWhenUp').attr('checked', 'checked'); 45 | } 46 | $('#inputNotifoUsername').val(response.notifoUsername); 47 | $('#inputNotifoApiSecret').val(response.notifoApiSecret); 48 | } 49 | 50 | /** 51 | * Executed when the form is submitted 52 | */ 53 | function _onSubmit(event) { 54 | 55 | var member = $('#inputMember').val(); 56 | var config = {}; 57 | $('#memberConfig input').each(function(i,input) { 58 | var value = ($(input).attr('type') == 'checkbox') ? 59 | $(input).is(':checked') : $(input).val(); 60 | config[$(input).attr('name')] = value; 61 | }); 62 | 63 | $.ajax({ 64 | url: '/api/member/'+member, 65 | type: 'POST', 66 | data: JSON.stringify(config), 67 | dataType: 'json', 68 | contentType: "application/json; charset=UTF-8", 69 | success: function( data ) { 70 | console.log(data); 71 | } 72 | }); 73 | } 74 | 75 | return { 76 | init: function() { 77 | _setupEventListeners(); 78 | $('#inputMember').select(); 79 | } 80 | }; 81 | 82 | })(); 83 | })(jQuery); 84 | -------------------------------------------------------------------------------- /src/main/webapp/js/main.js: -------------------------------------------------------------------------------- 1 | require(['js/vendor/less-1.1.0.min.js', 2 | 'js/vendor/jquery-1.6.1.min.js'], function() { 3 | require(['js/vendor/underscore-min.js', 4 | 'js/PushBotConfig.js', 5 | 'js/vendor/jquery.log.js', 6 | 'js/vendor/underscore.string.min.js', 7 | 'js/vendor/jquery.address-1.4.min.js', 8 | 'js/vendor/jquery.timeago.js', 9 | 'js/vendor/jquery.flot-0.1.pack.js', 10 | 'js/vendor/jquery.tmpl.min.js'], function() { 11 | require.ready(function() { 12 | PushBotConfig.init(); 13 | }); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/main/webapp/js/vendor/allplugins-require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 0.2.0 Copyright (c) 2010, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var require,define; 7 | (function(){function P(d){return Y.call(d)==="[object Function]"}function Q(d){return Y.call(d)==="[object Array]"}function R(d,e,j){for(var k in e)if(!(k in B)&&(!(k in d)||j))d[k]=e[k];return g}function Z(d,e,j){var k,o,l;for(k=0;l=e[k];k++){l=typeof l==="string"?{name:l}:l;o=l.location;if(j&&(!o||o.indexOf("/")!==0&&o.indexOf(":")===-1))l.location=j+"/"+(l.location||l.name);l.location=l.location||l.name;l.lib=l.lib||"lib";l.main=l.main||"main";d[l.name]=l}}function ka(d){function e(a,b){var c; 8 | if(a.charAt(0)===".")if(b){if(i.config.packages[b])b=[b];else{b=b.split("/");b=b.slice(0,b.length-1)}a=b.concat(a.split("/"));for(c=0;b=a[c];c++)if(b==="."){a.splice(c,1);c-=1}else if(b==="..")if(c===1)break;else if(c>1){a.splice(c-1,2);c-=2}a=a.join("/")}return a}function j(a,b){var c=a?a.indexOf("!"):-1,f=null;if(c!==-1){f=a.substring(0,c);a=a.substring(c+1,a.length)}if(a)a=e(a,b);if(f){f=e(f,b);f=la[f]||f}return{prefix:f,name:a,fullName:f?f+"!"+a:a}}function k(){var a=true,b=s.priorityWait,c,f; 9 | if(b){for(f=0;c=b[f];f++)if(!t[c]){a=false;break}a&&delete s.priorityWait}return a}function o(a){if(!i.jQuery)if((a=a||(typeof jQuery!=="undefined"?jQuery:null))&&"readyWait"in a){i.jQuery=a;if(!q.jquery&&!i.jQueryDef)q.jquery=a;if(i.scriptCount){a.readyWait+=1;i.jQueryIncremented=true}}}function l(a){return function(b){a.exports=b}}function S(a,b){return function(){var c=[].concat(ma.call(arguments,0));c.push(b);return a.apply(null,c)}}function $(a){var b=S(i.require,a);R(b,{nameToUrl:S(i.nameToUrl, 10 | a),toUrl:S(i.toUrl,a),ready:g.ready,isBrowser:g.isBrowser});return b}function aa(a){var b=a.prefix,c=a.fullName;if(!(D[c]||c in q)){if(b){i.plugins[b]=undefined;aa(j(b))}i.paused.push(a)}}function T(a){var b,c,f;b=a.callback;var h=a.fullName;f=[];var m=a.depArray;if(b&&P(b)){if(m)for(b=0;b0;n--){m=c.slice(0,n).join("/");if(f[m]){c.splice(0,n,f[m]);break}else if(m=h[m]){f=m.location+"/"+m.lib;if(a===m.name)f+="/"+ 20 | m.main;c.splice(0,n,f);break}}a=c.join("/")+(b||".js");a=(a.charAt(0)==="/"||a.match(/^\w+:/)?"":r.baseUrl)+a}return r.urlArgs?a+((a.indexOf("?")===-1?"?":"&")+r.urlArgs):a}};i.jQueryCheck=o;i.resume=E;return i}function sa(){var d,e,j;if(H&&H.readyState==="interactive")return H;d=document.getElementsByTagName("script");for(e=d.length-1;e>-1&&(j=d[e]);e--)if(j.readyState==="interactive")return H=j;return null}var ta=/(\/\*([\s\S]*?)\*\/|\/\/(.*)$)/mg,ua=/require\(["']([\w\!\-_\.\/]+)["']\)/g,Y=Object.prototype.toString, 21 | I=Array.prototype,ma=I.slice,ra=I.splice,w=!!(typeof window!=="undefined"&&navigator&&document),ea=!w&&typeof importScripts!=="undefined",va=w&&navigator.platform==="PLAYSTATION 3"?/^complete$/:/^(complete|loaded)$/,na="_r@@",B={},C={},M=[],H=null,wa=false,ga=false,la={text:"require/text",i18n:"require/i18n",order:"require/order"},g,u={},W,p,A,N,J,ha,v,ia,O,z,X,ja;if(typeof require!=="undefined")if(P(require))return;else u=require;g=require=function(d,e,j){var k="_",o;if(!Q(d)&&typeof d!=="string"){o= 22 | d;if(Q(e)){d=e;e=j}else d=[]}if(o&&o.context)k=o.context;j=C[k]||(C[k]=ka(k));o&&j.configure(o);return j.require(d,e)};g.version="0.2.0";g.isArray=Q;g.isFunction=P;g.mixin=R;g.jsExtRegExp=/^\/|:|\?|\.js$/;p=g.s={contexts:C,skipAsync:{},isPageLoaded:!w,readyCalls:[]};if(g.isAsync=g.isBrowser=w){A=p.head=document.getElementsByTagName("head")[0];if(N=document.getElementsByTagName("base")[0])A=p.head=N.parentNode}g.onError=function(d){throw d;};g.load=function(d,e){var j=d.contextName,k=d.urlFetched, 23 | o=d.loaded;wa=false;o[e]||(o[e]=false);o=d.nameToUrl(e);if(!k[o]){d.scriptCount+=1;g.attach(o,j,e);k[o]=true;if(d.jQuery&&!d.jQueryIncremented){d.jQuery.readyWait+=1;d.jQueryIncremented=true}}};define=g.def=function(d,e,j){var k;if(typeof d!=="string"){j=e;e=d;d=null}if(!g.isArray(e)){j=e;e=[]}if(!d&&!e.length&&g.isFunction(j))if(j.length){j.toString().replace(ta,"").replace(ua,function(o,l){e.push(l)});e=["require","exports","module"].concat(e)}if(ga){k=W||sa();if(!k)return g.onError(new Error("ERROR: No matching script interactive for "+ 24 | j));d||(d=k.getAttribute("data-requiremodule"));k=C[k.getAttribute("data-requirecontext")]}(k?k.defQueue:M).push([d,e,j])};g.execCb=function(d,e,j){return e.apply(null,j)};g.onScriptLoad=function(d){var e=d.currentTarget||d.srcElement,j;if(d.type==="load"||va.test(e.readyState)){H=null;d=e.getAttribute("data-requirecontext");j=e.getAttribute("data-requiremodule");C[d].completeLoad(j);e.removeEventListener?e.removeEventListener("load",g.onScriptLoad,false):e.detachEvent("onreadystatechange",g.onScriptLoad)}}; 25 | g.attach=function(d,e,j,k,o){var l;if(w){k=k||g.onScriptLoad;l=document.createElement("script");l.type=o||"text/javascript";l.charset="utf-8";l.async=!p.skipAsync[d];l.setAttribute("data-requirecontext",e);l.setAttribute("data-requiremodule",j);if(l.addEventListener)l.addEventListener("load",k,false);else{ga=true;l.attachEvent("onreadystatechange",k)}l.src=d;W=l;N?A.insertBefore(l,N):A.appendChild(l);W=null;return l}else if(ea){k=C[e];e=k.loaded;e[j]=false;importScripts(d);k.completeLoad(j)}return null}; 26 | p.baseUrl=u.baseUrl;if(w&&(!p.baseUrl||!A)){I=document.getElementsByTagName("script");ha=u.baseUrlMatch?u.baseUrlMatch:/(allplugins-)?require\.js(\W|$)/i;for(z=I.length-1;z>-1&&(J=I[z]);z--){if(!A)A=J.parentNode;if(!O&&(O=J.getAttribute("data-main"))){u.deps=u.deps?u.deps.concat(O):[O];if(!u.baseUrl&&(v=J.src)){v=v.split("/");v.pop();p.baseUrl=u.baseUrl=v.length?v.join("/"):"./"}}if(!p.baseUrl&&(v=J.src))if(ia=v.match(ha)){p.baseUrl=v.substring(0,ia.index);break}}}g.pageLoaded=function(){if(!p.isPageLoaded){p.isPageLoaded= 27 | true;X&&clearInterval(X);if(ja)document.readyState="complete";g.callReady()}};g.checkReadyState=function(){var d=p.contexts,e;for(e in d)if(!(e in B))if(d[e].waitCount)return;p.isDone=true;g.callReady()};g.callReady=function(){var d=p.readyCalls,e,j,k;if(p.isPageLoaded&&p.isDone){if(d.length){p.readyCalls=[];for(e=0;j=d[e];e++)j()}d=p.contexts;for(k in d)if(!(k in B)){e=d[k];if(e.jQueryIncremented){e.jQuery.readyWait-=1;e.jQueryIncremented=false}}}};g.ready=function(d){p.isPageLoaded&&p.isDone?d(): 28 | p.readyCalls.push(d);return g};if(w){if(document.addEventListener){document.addEventListener("DOMContentLoaded",g.pageLoaded,false);window.addEventListener("load",g.pageLoaded,false);if(!document.readyState){ja=true;document.readyState="loading"}}else if(window.attachEvent){window.attachEvent("onload",g.pageLoaded);if(self===self.top)X=setInterval(function(){try{if(document.body){document.documentElement.doScroll("left");g.pageLoaded()}}catch(d){}},30)}document.readyState==="complete"&&g.pageLoaded()}g(u); 29 | typeof setTimeout!=="undefined"&&setTimeout(function(){var d=p.contexts[u.context||"_"];d.jQueryCheck();d.scriptCount||d.resume()},0)})(); 30 | -------------------------------------------------------------------------------- /src/main/webapp/js/vendor/jquery.address-1.4.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery Address Plugin v1.4 3 | * http://www.asual.com/jquery/address/ 4 | * 5 | * Copyright (c) 2009-2010 Rostislav Hristov 6 | * Dual licensed under the MIT or GPL Version 2 licenses. 7 | * http://jquery.org/license 8 | * 9 | * Date: 2011-05-04 14:22:12 +0300 (Wed, 04 May 2011) 10 | */ 11 | (function(c){c.address=function(){var v=function(a){c(c.address).trigger(c.extend(c.Event(a),function(){for(var b={},e=c.address.parameterNames(),f=0,p=e.length;f"+n.title.replace("'","\\'")+"