You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Twitter.java 62KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418
  1. /*
  2. * Copyright (c) 2006-2009 Chris Smith, Shane Mc Cormack, Gregory Holmes
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc.addons.parser_twitter;
  23. import com.dmdirc.addons.parser_twitter.api.TwitterAPI;
  24. import com.dmdirc.addons.parser_twitter.api.TwitterErrorHandler;
  25. import com.dmdirc.addons.parser_twitter.api.TwitterMessage;
  26. import com.dmdirc.addons.parser_twitter.api.TwitterStatus;
  27. import com.dmdirc.addons.parser_twitter.api.TwitterUser;
  28. import com.dmdirc.parser.common.CallbackManager;
  29. import com.dmdirc.parser.common.DefaultStringConverter;
  30. import com.dmdirc.parser.common.IgnoreList;
  31. import com.dmdirc.parser.common.MyInfo;
  32. import com.dmdirc.parser.common.QueuePriority;
  33. import com.dmdirc.parser.interfaces.ChannelClientInfo;
  34. import com.dmdirc.parser.interfaces.ChannelInfo;
  35. import com.dmdirc.parser.interfaces.ClientInfo;
  36. import com.dmdirc.parser.interfaces.LocalClientInfo;
  37. import com.dmdirc.parser.interfaces.Parser;
  38. import com.dmdirc.parser.interfaces.callbacks.ChannelMessageListener;
  39. import com.dmdirc.parser.interfaces.StringConverter;
  40. import com.dmdirc.parser.interfaces.callbacks.AuthNoticeListener;
  41. import com.dmdirc.parser.interfaces.callbacks.ChannelModeChangeListener;
  42. import com.dmdirc.parser.interfaces.callbacks.ChannelNamesListener;
  43. import com.dmdirc.parser.interfaces.callbacks.ChannelSelfJoinListener;
  44. import com.dmdirc.parser.interfaces.callbacks.ChannelTopicListener;
  45. import com.dmdirc.parser.interfaces.callbacks.MotdEndListener;
  46. import com.dmdirc.parser.interfaces.callbacks.MotdLineListener;
  47. import com.dmdirc.parser.interfaces.callbacks.MotdStartListener;
  48. import com.dmdirc.parser.interfaces.callbacks.NetworkDetectedListener;
  49. import com.dmdirc.parser.interfaces.callbacks.NickChangeListener;
  50. import com.dmdirc.parser.interfaces.callbacks.NumericListener;
  51. import com.dmdirc.parser.interfaces.callbacks.Post005Listener;
  52. import com.dmdirc.parser.interfaces.callbacks.PrivateMessageListener;
  53. import com.dmdirc.parser.interfaces.callbacks.UnknownMessageListener;
  54. import com.dmdirc.parser.interfaces.callbacks.PrivateNoticeListener;
  55. import com.dmdirc.parser.interfaces.callbacks.ServerReadyListener;
  56. import com.dmdirc.parser.interfaces.callbacks.UserModeDiscoveryListener;
  57. import com.dmdirc.parser.interfaces.callbacks.SocketCloseListener;
  58. import com.dmdirc.ui.messages.Styliser;
  59. import java.lang.reflect.InvocationTargetException;
  60. import java.util.ArrayList;
  61. import java.util.Collection;
  62. import java.util.Collections;
  63. import java.util.HashMap;
  64. import java.util.List;
  65. import java.util.Map;
  66. import com.dmdirc.interfaces.ConfigChangeListener;
  67. import com.dmdirc.addons.parser_twitter.api.TwitterException;
  68. import com.dmdirc.addons.parser_twitter.api.TwitterRawHandler;
  69. import com.dmdirc.config.ConfigManager;
  70. import com.dmdirc.config.IdentityManager;
  71. import com.dmdirc.logger.ErrorManager;
  72. import com.dmdirc.parser.interfaces.callbacks.ChannelJoinListener;
  73. import com.dmdirc.parser.interfaces.callbacks.ChannelKickListener;
  74. import com.dmdirc.parser.interfaces.callbacks.DataInListener;
  75. import com.dmdirc.parser.interfaces.callbacks.DataOutListener;
  76. import com.dmdirc.parser.interfaces.callbacks.DebugInfoListener;
  77. import java.lang.reflect.Method;
  78. import java.net.URI;
  79. import java.util.Calendar;
  80. /**
  81. * Twitter Parser for DMDirc.
  82. *
  83. * @author shane
  84. */
  85. public class Twitter implements Parser, TwitterErrorHandler, TwitterRawHandler, ConfigChangeListener {
  86. /** Are we connected? */
  87. private boolean connected = false;
  88. /** Our owner plugin */
  89. private TwitterPlugin myPlugin = null;
  90. /** Twitter API. */
  91. private TwitterAPI api = new TwitterAPI("", "", "", false, -1, false);
  92. /** Channels we are in. */
  93. private final Map<String, TwitterChannelInfo> channels = new HashMap<String, TwitterChannelInfo>();
  94. /** Clients we know. */
  95. private final Map<String, TwitterClientInfo> clients = new HashMap<String, TwitterClientInfo>();
  96. /** When did we last query the API? */
  97. private long lastQueryTime = 0;
  98. /** Username for twitter. */
  99. private String myUsername;
  100. /** Password for twitter if not able to use oauth. */
  101. private String myPassword;
  102. /** Callback Manager for Twitter. */
  103. private CallbackManager<Twitter> myCallbackManager = new TwitterCallbackManager(this);
  104. /** String Convertor. */
  105. private DefaultStringConverter myStringConverter = new DefaultStringConverter();
  106. /** Ignore list (unused). */
  107. private IgnoreList myIgnoreList = new IgnoreList();
  108. /** Myself. */
  109. private TwitterClientInfo myself = null;
  110. /** List of currently active twitter parsers. */
  111. protected static List<Twitter> currentParsers = new ArrayList<Twitter>();
  112. /** Are we waiting for authentication? */
  113. private boolean wantAuth = false;
  114. /** Server we are connecting to. */
  115. private final String myServerName;
  116. /** API Address to use. */
  117. private final String apiAddress;
  118. /** Are we using API Versioning? */
  119. private boolean apiVersioning = false;
  120. /** What API Version do we want? */
  121. private int apiVersion = -1;
  122. /** Address that created us. */
  123. private final URI myAddress;
  124. /** Main Channel Name */
  125. private final String mainChannelName;
  126. /** Config Manager for this parser. */
  127. private ConfigManager myConfigManager = null;
  128. /** Map to store misc stuff in. */
  129. private Map<Object, Object> myMap = new HashMap<Object, Object>();
  130. /**
  131. * Create a new Twitter Parser!
  132. *
  133. * @param myInfo The client information to use
  134. * @param address The address of the server to connect to
  135. * @param myPlugin Plugin that created this parser
  136. */
  137. protected Twitter(final MyInfo myInfo, final URI address, final TwitterPlugin myPlugin) {
  138. final String[] bits = address.getUserInfo().split(":");
  139. this.myUsername = bits[0];
  140. this.myPassword = (bits.length > 1) ? bits[1] : "";
  141. this.myPlugin = myPlugin;
  142. this.myServerName = address.getHost().toLowerCase();
  143. this.myAddress = address;
  144. resetState(true);
  145. if (getConfigManager().hasOptionString(myPlugin.getDomain(), "api.address."+myServerName)) {
  146. this.apiAddress = getConfigManager().getOption(myPlugin.getDomain(), "api.address."+myServerName);
  147. } else {
  148. this.apiAddress = this.myServerName + address.getPath();
  149. }
  150. if (getConfigManager().hasOptionBool(myPlugin.getDomain(), "api.versioned."+myServerName)) {
  151. this.apiVersioning = getConfigManager().getOptionBool(myPlugin.getDomain(), "api.versioned."+myServerName);
  152. if (getConfigManager().hasOptionInt(myPlugin.getDomain(), "api.version."+myServerName)) {
  153. this.apiVersion = getConfigManager().getOptionInt(myPlugin.getDomain(), "api.version."+myServerName);
  154. }
  155. }
  156. this.mainChannelName = "&twitter";
  157. }
  158. /** {@inheritDoc} */
  159. @Override
  160. public void disconnect(final String message) {
  161. connected = false;
  162. currentParsers.remove(this);
  163. api = new TwitterAPI("", "", "", false, -1, false);
  164. getCallbackManager().getCallbackType(SocketCloseListener.class).call();
  165. }
  166. /** {@inheritDoc} */
  167. @Override
  168. public void joinChannel(final String channel) {
  169. joinChannel(channel, "");
  170. }
  171. /** {@inheritDoc} */
  172. @Override
  173. public void joinChannel(final String channel, final String key) {
  174. if (isValidChannelName(channel) && getChannel(channel) == null && !channel.equalsIgnoreCase(mainChannelName)) {
  175. final TwitterChannelInfo newChannel = new TwitterChannelInfo(channel, this);
  176. newChannel.addChannelClient(new TwitterChannelClientInfo(newChannel, myself));
  177. if (channel.matches("^&[0-9]+$")) {
  178. try {
  179. long id = Long.parseLong(channel.substring(1));
  180. final TwitterStatus status = api.getStatus(id);
  181. if (status != null) {
  182. if (status.getReplyTo() > 0) {
  183. newChannel.setLocalTopic(status.getText()+" [Reply to: &"+status.getReplyTo()+"]");
  184. } else {
  185. newChannel.setLocalTopic(status.getText());
  186. }
  187. newChannel.setTopicSetter(status.getUser().getScreenName());
  188. newChannel.setTopicTime(status.getTime());
  189. final TwitterClientInfo client = (TwitterClientInfo) getClient(status.getUser().getScreenName());
  190. if (client.isFake()) {
  191. client.setFake(false);
  192. clients.put(client.getNickname().toLowerCase(), client);
  193. }
  194. newChannel.addChannelClient(new TwitterChannelClientInfo(newChannel, client));
  195. } else {
  196. newChannel.setLocalTopic("Unknown status, or you do not have access to see it.");
  197. }
  198. synchronized (channels) {
  199. channels.put(channel, newChannel);
  200. }
  201. } catch (NumberFormatException nfe) { }
  202. }
  203. doJoinChannel(newChannel);
  204. } else {
  205. sendNumericOutput(474, new String[]{":"+myServerName, "474", myself.getNickname(), channel, "Cannot join channel - name is not valid, or you are already there."});
  206. }
  207. }
  208. /**
  209. * Remove a channel from the known channels list.
  210. *
  211. * @param channel
  212. */
  213. protected void partChannel(final TwitterChannelInfo channel) {
  214. synchronized (channels) {
  215. channels.remove(channel.getName());
  216. }
  217. }
  218. /** {@inheritDoc} */
  219. @Override
  220. public ChannelInfo getChannel(final String channel) {
  221. synchronized (channels) {
  222. return channels.containsKey(channel.toLowerCase()) ? channels.get(channel.toLowerCase()) : null;
  223. }
  224. }
  225. /** {@inheritDoc} */
  226. @Override
  227. public Collection<? extends ChannelInfo> getChannels() {
  228. return new ArrayList<TwitterChannelInfo>(channels.values());
  229. }
  230. /** {@inheritDoc} */
  231. @Override
  232. public void setBindIP(final String ip) {
  233. return;
  234. }
  235. /** {@inheritDoc} */
  236. @Override
  237. public int getMaxLength(final String type, final String target) {
  238. return 140;
  239. }
  240. /** {@inheritDoc} */
  241. @Override
  242. public LocalClientInfo getLocalClient() {
  243. return myself;
  244. }
  245. /** {@inheritDoc} */
  246. @Override
  247. public ClientInfo getClient(final String details) {
  248. final String client = TwitterClientInfo.parseHost(details);
  249. return clients.containsKey(client.toLowerCase()) ? clients.get(client.toLowerCase()) : new TwitterClientInfo(details, this).setFake(true);
  250. }
  251. /**
  252. * Tokenise a line.
  253. * splits by " " up to the first " :" everything after this is a single token
  254. *
  255. * @param line Line to tokenise
  256. * @return Array of tokens
  257. */
  258. public static String[] tokeniseLine(final String line) {
  259. if (line == null) {
  260. return new String[]{"", }; // Return empty string[]
  261. }
  262. final int lastarg = line.indexOf(" :");
  263. String[] tokens;
  264. if (lastarg > -1) {
  265. final String[] temp = line.substring(0, lastarg).split(" ");
  266. tokens = new String[temp.length + 1];
  267. System.arraycopy(temp, 0, tokens, 0, temp.length);
  268. tokens[temp.length] = line.substring(lastarg + 2);
  269. } else {
  270. tokens = line.split(" ");
  271. }
  272. return tokens;
  273. }
  274. /** {@inheritDoc} */
  275. @Override
  276. public void sendRawMessage(final String message) {
  277. sendRawMessage(message, QueuePriority.NORMAL);
  278. }
  279. /** {@inheritDoc} */
  280. @Override
  281. public void sendRawMessage(final String message, final QueuePriority priority) {
  282. // TODO: Parse some lines in order to fake IRC.
  283. final String[] bits = tokeniseLine(message);
  284. if (bits[0].equalsIgnoreCase("JOIN") && bits.length > 1) {
  285. joinChannel(bits[1]);
  286. } else if (bits[0].equalsIgnoreCase("WHOIS") && bits.length > 1) {
  287. if (bits[1].equalsIgnoreCase(myServerName)) {
  288. sendNumericOutput(311, new String[]{":"+myServerName, "311", myself.getNickname(), bits[1], "user", myServerName, "*", "Psuedo-User for DMDirc "+myServerName+" plugin"});
  289. sendNumericOutput(312, new String[]{":"+myServerName, "312", myself.getNickname(), bits[1], myServerName, "DMDirc "+myServerName+" plugin"});
  290. } else {
  291. final boolean forced = (bits.length > 2 && bits[1].equalsIgnoreCase(bits[2]));
  292. final TwitterUser user = (forced) ? api.getUser(bits[1], true) : api.getCachedUser(bits[1]);
  293. if (user == null) {
  294. final String reason = (forced) ? "No such user found, see http://"+myAddress.getHost()+"/"+user.getScreenName() : "No such user found in cache, try /WHOIS "+bits[1]+" "+bits[1]+" to poll twitter (uses 1 API call) or try http://"+myAddress.getHost()+"/"+user.getScreenName();
  295. getCallbackManager().getCallbackType(NumericListener.class).call(401, new String[]{":"+myServerName, "401", myself.getNickname(), bits[1], reason});
  296. } else {
  297. // Time since last update
  298. final long secondsIdle = ((user.getStatus() != null) ? System.currentTimeMillis() - user.getStatus().getTime() : user.getRegisteredTime()) / 1000;
  299. final long signonTime = user.getRegisteredTime() / 1000;
  300. getCallbackManager().getCallbackType(NumericListener.class).call(311, new String[]{":"+myServerName, "311", myself.getNickname(), bits[1], "user", myServerName, "*", user.getRealName()+" (http://"+myAddress.getHost()+"/"+user.getScreenName()+")"});
  301. final TwitterClientInfo client = (TwitterClientInfo)getClient(bits[1]);
  302. if (client != null) {
  303. final StringBuilder channelList = new StringBuilder();
  304. for (ChannelClientInfo cci : client.getChannelClients()) {
  305. if (channelList.length() > 0) { channelList.append(" "); }
  306. channelList.append(cci.getImportantModePrefix()+cci.getChannel().getName());
  307. }
  308. if (channelList.length() > 0) {
  309. getCallbackManager().getCallbackType(NumericListener.class).call(319, new String[]{":"+myServerName, "319", myself.getNickname(), bits[1], channelList.toString()});
  310. }
  311. }
  312. // AWAY Message Abuse!
  313. getCallbackManager().getCallbackType(NumericListener.class).call(301, new String[]{":"+myServerName, "301", myself.getNickname(), bits[1], "URL: "+user.getURL()});
  314. getCallbackManager().getCallbackType(NumericListener.class).call(301, new String[]{":"+myServerName, "301", myself.getNickname(), bits[1], "Bio: "+user.getDescription()});
  315. getCallbackManager().getCallbackType(NumericListener.class).call(301, new String[]{":"+myServerName, "301", myself.getNickname(), bits[1], "Location: "+user.getLocation()});
  316. getCallbackManager().getCallbackType(NumericListener.class).call(301, new String[]{":"+myServerName, "301", myself.getNickname(), bits[1], "Status: "+user.getStatus().getText()});
  317. if (bits[1].equalsIgnoreCase(myself.getNickname())) {
  318. final Long[] apiCalls = api.getRemainingApiCalls();
  319. getCallbackManager().getCallbackType(NumericListener.class).call(301, new String[]{":"+myServerName, "301", myself.getNickname(), bits[1], "API Allowance: "+apiCalls[1]});
  320. getCallbackManager().getCallbackType(NumericListener.class).call(301, new String[]{":"+myServerName, "301", myself.getNickname(), bits[1], "API Allowance Remaining: "+apiCalls[0]});
  321. getCallbackManager().getCallbackType(NumericListener.class).call(301, new String[]{":"+myServerName, "301", myself.getNickname(), bits[1], "API Calls Used: "+apiCalls[3]});
  322. }
  323. getCallbackManager().getCallbackType(NumericListener.class).call(312, new String[]{":"+myServerName, "312", myself.getNickname(), bits[1], myServerName, "DMDirc "+myServerName+" plugin"});
  324. getCallbackManager().getCallbackType(NumericListener.class).call(317, new String[]{":"+myServerName, "317", myself.getNickname(), bits[1], Long.toString(secondsIdle), Long.toString(signonTime), "seconds idle, signon time"});
  325. }
  326. }
  327. getCallbackManager().getCallbackType(NumericListener.class).call(318, new String[]{":"+myServerName, "318", myself.getNickname(), bits[1], "End of /WHOIS list."});
  328. } else if (bits[0].equalsIgnoreCase("INVITE") && bits.length > 2) {
  329. if (bits[2].equalsIgnoreCase(mainChannelName)) {
  330. final TwitterUser user = api.addFriend(bits[1]);
  331. final TwitterChannelInfo channel = (TwitterChannelInfo) getChannel(bits[2]);
  332. if (channel != null && user != null) {
  333. final TwitterClientInfo ci = new TwitterClientInfo(user.getScreenName(), this);
  334. clients.put(ci.getNickname().toLowerCase(), ci);
  335. final TwitterChannelClientInfo cci = new TwitterChannelClientInfo(channel, ci);
  336. channel.addChannelClient(cci);
  337. getCallbackManager().getCallbackType(ChannelJoinListener.class).call(channel, cci);
  338. }
  339. } else {
  340. getCallbackManager().getCallbackType(NumericListener.class).call(474, new String[]{":"+myServerName, "482", myself.getNickname(), bits[1], "You can't do that here."});
  341. }
  342. } else if (bits[0].equalsIgnoreCase("KICK") && bits.length > 2) {
  343. if (bits[1].equalsIgnoreCase(mainChannelName)) {
  344. final TwitterChannelInfo channel = (TwitterChannelInfo) getChannel(bits[1]);
  345. if (channel != null) {
  346. final TwitterChannelClientInfo cci = (TwitterChannelClientInfo) channel.getChannelClient(bits[2]);
  347. if (cci != null) { cci.kick("Bye"); }
  348. final TwitterChannelClientInfo mycci = (TwitterChannelClientInfo) channel.getChannelClient(myUsername);
  349. getCallbackManager().getCallbackType(ChannelKickListener.class).call(channel, cci, mycci, "", myUsername);
  350. }
  351. } else {
  352. getCallbackManager().getCallbackType(NumericListener.class).call(474, new String[]{":"+myServerName, "482", myself.getNickname(), bits[1], "You can't do that here."});
  353. }
  354. } else {
  355. getCallbackManager().getCallbackType(NumericListener.class).call(474, new String[]{":"+myServerName, "421", myself.getNickname(), bits[0], "Unknown Command - "+message});
  356. }
  357. }
  358. /** {@inheritDoc} */
  359. @Override
  360. public boolean isValidChannelName(final String name) {
  361. return (name.matches("^&[0-9]+$") || name.equalsIgnoreCase(mainChannelName) || name.startsWith("#"));
  362. }
  363. /** {@inheritDoc} */
  364. @Override
  365. public String getServerName() {
  366. return myServerName + "/" + myself.getNickname();
  367. }
  368. /** {@inheritDoc} */
  369. @Override
  370. public String getNetworkName() {
  371. return myServerName + "/" + myself.getNickname();
  372. }
  373. /** {@inheritDoc} */
  374. @Override
  375. public String getServerSoftware() {
  376. return myServerName;
  377. }
  378. /** {@inheritDoc} */
  379. @Override
  380. public String getServerSoftwareType() {
  381. return "twitter";
  382. }
  383. /** {@inheritDoc} */
  384. @Override
  385. public int getMaxTopicLength() {
  386. return 140;
  387. }
  388. /** {@inheritDoc} */
  389. @Override
  390. public String getBooleanChannelModes() {
  391. return "";
  392. }
  393. /** {@inheritDoc} */
  394. @Override
  395. public String getListChannelModes() {
  396. return "b";
  397. }
  398. /** {@inheritDoc} */
  399. @Override
  400. public int getMaxListModes(final char mode) {
  401. return 0;
  402. }
  403. /** {@inheritDoc} */
  404. @Override
  405. public boolean isUserSettable(final char mode) {
  406. return mode == 'b';
  407. }
  408. /** {@inheritDoc} */
  409. @Override
  410. public String getParameterChannelModes() {
  411. return "";
  412. }
  413. /** {@inheritDoc} */
  414. @Override
  415. public String getDoubleParameterChannelModes() {
  416. return "";
  417. }
  418. /** {@inheritDoc} */
  419. @Override
  420. public String getUserModes() {
  421. return "";
  422. }
  423. /** {@inheritDoc} */
  424. @Override
  425. public String getChannelUserModes() {
  426. return "ov";
  427. }
  428. /** {@inheritDoc} */
  429. @Override
  430. public CallbackManager<? extends Parser> getCallbackManager() {
  431. return myCallbackManager;
  432. }
  433. /** {@inheritDoc} */
  434. @Override
  435. public long getServerLatency() {
  436. return 0;
  437. }
  438. /** {@inheritDoc} */
  439. @Override
  440. public String getBindIP() {
  441. return "";
  442. }
  443. /** {@inheritDoc} */
  444. @Override
  445. public Map<Object, Object> getMap() {
  446. return myMap;
  447. }
  448. /** {@inheritDoc} */
  449. @Override
  450. public URI getURI() {
  451. return myAddress;
  452. }
  453. /** {@inheritDoc} */
  454. @Override
  455. public boolean compareURI(final URI uri) {
  456. return myAddress.equals(uri);
  457. }
  458. /** {@inheritDoc} */
  459. @Override
  460. public void sendCTCP(final String target, final String type, final String message) {
  461. if (wantAuth) {
  462. sendPrivateNotice("DMDirc has not been authorised to use this account yet.");
  463. } else if (target.matches("^&[0-9]+$")) {
  464. try {
  465. long id = Long.parseLong(target.substring(1));
  466. if (type.equalsIgnoreCase("retweet") || type.equalsIgnoreCase("rt")) {
  467. final TwitterStatus status = api.getStatus(id);
  468. if (status != null) {
  469. sendPrivateNotice("Retweeting: <"+status.getUser().getScreenName()+"> "+status.getText());
  470. if (api.retweetStatus(status)) {
  471. sendPrivateNotice("Retweet was successful.");
  472. final TwitterChannelInfo channel = (TwitterChannelInfo) this.getChannel(mainChannelName);
  473. checkTopic(channel, myself.getUser().getStatus());
  474. } else {
  475. sendPrivateNotice("Retweeting Failed.");
  476. }
  477. } else {
  478. sendPrivateNotice("Invalid Tweet ID.");
  479. }
  480. } else if (type.equalsIgnoreCase("delete") || type.equalsIgnoreCase("del")) {
  481. final TwitterStatus status = api.getStatus(id);
  482. if (status != null) {
  483. sendPrivateNotice("Deleting: <"+status.getUser().getScreenName()+"> "+status.getText());
  484. if (api.deleteStatus(status)) {
  485. sendPrivateNotice("Deleting was successful, deleted tweets will still be accessible for some time.");
  486. final TwitterChannelInfo channel = (TwitterChannelInfo) this.getChannel(mainChannelName);
  487. checkTopic(channel, myself.getUser().getStatus());
  488. } else {
  489. sendPrivateNotice("Deleting Failed.");
  490. }
  491. } else {
  492. sendPrivateNotice("Invalid Tweet ID.");
  493. }
  494. }
  495. } catch (NumberFormatException nfe) {
  496. sendPrivateNotice("Invalid Tweet ID.");
  497. }
  498. } else {
  499. sendPrivateNotice("This parser does not support CTCPs.");
  500. }
  501. }
  502. /** {@inheritDoc} */
  503. @Override
  504. public void sendCTCPReply(final String target, final String type, final String message) {
  505. sendPrivateNotice("This parser does not support CTCP replies.");
  506. }
  507. /** {@inheritDoc} */
  508. @Override
  509. public void sendMessage(final String target, final String message) {
  510. final TwitterChannelInfo channel = (TwitterChannelInfo) this.getChannel(target);
  511. if (target.equalsIgnoreCase(mainChannelName)) {
  512. if (wantAuth) {
  513. final String[] bits = message.split(" ");
  514. if (bits[0].equalsIgnoreCase("usepw")) {
  515. sendChannelMessage(channel, "Switching to once-off password authentication, please enter your password.");
  516. api.setUseOAuth(false);
  517. return;
  518. }
  519. try {
  520. if (api.useOAuth()) {
  521. api.setAccessPin(bits[0]);
  522. if (api.isAllowed(true)) {
  523. IdentityManager.getConfigIdentity().setOption(myPlugin.getDomain(), "token-"+myServerName+"-"+myUsername, api.getToken());
  524. IdentityManager.getConfigIdentity().setOption(myPlugin.getDomain(), "tokenSecret-"+myServerName+"-"+myUsername, api.getTokenSecret());
  525. sendChannelMessage(channel, "Thank you for authorising DMDirc.");
  526. updateTwitterChannel();
  527. wantAuth = false;
  528. } else {
  529. sendChannelMessage(channel, "Authorising DMDirc failed, please try again: "+api.getOAuthURL());
  530. }
  531. } else {
  532. api.setPassword(message);
  533. if (api.isAllowed(true)) {
  534. sendChannelMessage(channel, "Password accepted. Please note you will need to do this every time unless your password is given in the URL.");
  535. updateTwitterChannel();
  536. wantAuth = false;
  537. } else {
  538. sendChannelMessage(channel, "Password seems incorrect, please try again.");
  539. }
  540. }
  541. } catch (TwitterException te) {
  542. sendChannelMessage(channel, "There was a problem authorising DMDirc ("+te.getCause().getMessage()+").");
  543. sendChannelMessage(channel, "Please try again: "+api.getOAuthURL());
  544. }
  545. } else {
  546. if (setStatus(message)) {
  547. sendPrivateNotice("Setting status ok.");
  548. } else {
  549. sendPrivateNotice("Setting status failed.");
  550. }
  551. }
  552. } else if (wantAuth) {
  553. sendPrivateNotice("DMDirc has not been authorised to use this account yet.");
  554. } else if (target.matches("^&[0-9]+$")) {
  555. try {
  556. long id = Long.parseLong(target.substring(1));
  557. if (setStatus(message, id)) {
  558. sendPrivateNotice("Setting status ok.");
  559. } else {
  560. sendPrivateNotice("Setting status failed.");
  561. }
  562. } catch (NumberFormatException nfe) { }
  563. } else if (!target.matches("^#.+$")) {
  564. if (api.newDirectMessage(target, message)) {
  565. sendPrivateNotice("Sending Direct Message to '"+target+"' was successful.");
  566. } else {
  567. sendPrivateNotice("Sending Direct Message to '"+target+"' failed.");
  568. }
  569. } else {
  570. sendPrivateNotice("Messages to '"+target+"' are not currently supported.");
  571. }
  572. }
  573. /** {@inheritDoc} */
  574. @Override
  575. public void sendNotice(final String target, final String message) {
  576. sendPrivateNotice("This parser does not support notices.");
  577. }
  578. /** {@inheritDoc} */
  579. @Override
  580. public void sendAction(final String target, final String message) {
  581. sendPrivateNotice("This parser does not support CTCPs.");
  582. }
  583. /** {@inheritDoc} */
  584. @Override
  585. public String getLastLine() {
  586. return "";
  587. }
  588. /** {@inheritDoc} */
  589. @Override
  590. public String[] parseHostmask(final String hostmask) {
  591. return TwitterClientInfo.parseHostFull(hostmask, myPlugin, this);
  592. }
  593. /** {@inheritDoc} */
  594. @Override
  595. public int getLocalPort() {
  596. return api.getPort();
  597. }
  598. /** {@inheritDoc} */
  599. @Override
  600. public long getPingTime() {
  601. return System.currentTimeMillis() - lastQueryTime;
  602. }
  603. /** {@inheritDoc} */
  604. @Override
  605. public void setPingTimerInterval(final long newValue) { /* Do Nothing. */ }
  606. /** {@inheritDoc} */
  607. @Override
  608. public long getPingTimerInterval() {
  609. return -1;
  610. }
  611. /** {@inheritDoc} */
  612. @Override
  613. public void setPingTimerFraction(final int newValue) { /* Do Nothing. */ }
  614. /** {@inheritDoc} */
  615. @Override
  616. public int getPingTimerFraction() {
  617. return -1;
  618. }
  619. /**
  620. * Send a notice to the client.
  621. *
  622. * @param message Message to send.
  623. */
  624. protected void sendPrivateNotice(final String message) {
  625. getCallbackManager().getCallbackType(PrivateNoticeListener.class).call(message, myServerName);
  626. }
  627. /**
  628. * Send a PM to the client.
  629. *
  630. * @param message Message to send.
  631. */
  632. private void sendPrivateMessage(final String message) {
  633. sendPrivateMessage(message, myServerName);
  634. }
  635. /**
  636. * Send a PM to the client.
  637. *
  638. * @param message Message to send.
  639. * @param hostname Who is the message from?
  640. */
  641. private void sendPrivateMessage(final String message, final String hostname) {
  642. sendPrivateMessage(message, hostname, myUsername);
  643. }
  644. /**
  645. * Send a PM to the client.
  646. *
  647. * @param message Message to send.
  648. * @param hostname Who is the message from?
  649. * @param target Who is the message to?
  650. */
  651. private void sendPrivateMessage(final String message, final String hostname, final String target) {
  652. if (hostname.equalsIgnoreCase(myUsername)) {
  653. getCallbackManager().getCallbackType(UnknownMessageListener.class).call(message, target, hostname);
  654. } else {
  655. getCallbackManager().getCallbackType(PrivateMessageListener.class).call(message, hostname);
  656. }
  657. }
  658. /**
  659. * Send a message to the given channel.
  660. *
  661. * @param channel Channel to send message to
  662. * @param message Message to send.
  663. */
  664. private void sendChannelMessage(final ChannelInfo channel, final String message) {
  665. sendChannelMessage(channel, message, myServerName);
  666. }
  667. /**
  668. * Send a message to the given channel.
  669. *
  670. * @param channel Channel to send message to
  671. * @param message Message to send.
  672. * @param hostname Hostname that the message is from.
  673. */
  674. private void sendChannelMessage(final ChannelInfo channel, final String message, final String hostname) {
  675. sendChannelMessage(channel, message, null, hostname);
  676. }
  677. /**
  678. * Send a message to the given channel.
  679. *
  680. * @param channel Channel to send message to
  681. * @param message Message to send.
  682. * @param cci Channel Client to send from
  683. * @param hostname Hostname that the message is from.
  684. */
  685. private void sendChannelMessage(final ChannelInfo channel, final String message, final ChannelClientInfo cci, final String hostname) {
  686. getCallbackManager().getCallbackType(ChannelMessageListener.class).call(channel, cci, message, hostname);
  687. }
  688. /**
  689. * Show the user an ascii failwhale!
  690. */
  691. public void showFailWhale() {
  692. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5 ");
  693. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5 W W W ");
  694. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5 W W W W ");
  695. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5 '."+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5 W ");
  696. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5 .-\"\"-._ \\ \\.--| ");
  697. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5 / \"-..__) .-' ");
  698. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5 | _ / ");
  699. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5 \\'-.__, .__.,' ");
  700. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5 `'----'._\\--' ");
  701. sendPrivateNotice(""+Styliser.CODE_FIXED+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V"+Styliser.CODE_HEXCOLOUR+"FFFFFF,71C5C5V"+Styliser.CODE_HEXCOLOUR+"EB5405,71C5C5V");
  702. }
  703. /**
  704. * Check if the given user is known on the channel, and add them if they
  705. * are not.
  706. *
  707. * @param user User to check
  708. * @return true if user was already on the channel, false if they were added.
  709. */
  710. private boolean checkUserOnChannel(final TwitterUser user) {
  711. final TwitterChannelInfo channel = (TwitterChannelInfo) this.getChannel(mainChannelName);
  712. if (channel.getChannelClient(user.getScreenName()) == null) {
  713. final TwitterClientInfo ci = new TwitterClientInfo(user.getScreenName(), this);
  714. clients.put(ci.getNickname().toLowerCase(), ci);
  715. final TwitterChannelClientInfo cci = new TwitterChannelClientInfo(channel, ci);
  716. channel.addChannelClient(cci);
  717. getCallbackManager().getCallbackType(ChannelJoinListener.class).call(channel, cci);
  718. return false;
  719. } else {
  720. return true;
  721. }
  722. }
  723. /**
  724. * Send a Debug Message using the parser debug api.
  725. *
  726. * @param code Debug Code for the message.
  727. * @param message Content of the message.
  728. */
  729. private void doDebug(final Debug code, final String message) {
  730. final boolean debug = getConfigManager().getOptionBool(myPlugin.getDomain(), "debugEnabled");
  731. if (debug) {
  732. getCallbackManager().getCallbackType(DebugInfoListener.class).call(code.ordinal(), message);
  733. }
  734. }
  735. /**
  736. * Run the twitter parser.
  737. */
  738. @Override
  739. public void run() {
  740. resetState();
  741. // Get the consumerKey and consumerSecret for this server if known
  742. // else default to our twitter key and secret
  743. final String consumerKey;
  744. final String consumerSecret;
  745. if (getConfigManager().hasOptionString(myPlugin.getDomain(), "consumerKey-"+myServerName)) {
  746. consumerKey = getConfigManager().getOption(myPlugin.getDomain(), "consumerKey-"+myServerName);
  747. } else { consumerKey = "qftK3mAbLfbWWHf8shiyjw"; }
  748. if (getConfigManager().hasOptionString(myPlugin.getDomain(), "consumerSecret-"+myServerName)) {
  749. consumerSecret = getConfigManager().getOption(myPlugin.getDomain(), "consumerSecret-"+myServerName);
  750. } else { consumerSecret = "flPr2TJGp4795DeTu4VkUlNLX8g25SpXWXZ7SKW0Bg"; }
  751. final String token;
  752. final String tokenSecret;
  753. if (getConfigManager().hasOptionString(myPlugin.getDomain(), "token-"+myServerName+"-"+myUsername)) {
  754. token = getConfigManager().getOption(myPlugin.getDomain(), "token-"+myServerName+"-"+myUsername);
  755. } else { token = ""; }
  756. if (getConfigManager().hasOptionString(myPlugin.getDomain(), "tokenSecret-"+myServerName+"-"+myUsername)) {
  757. tokenSecret = getConfigManager().getOption(myPlugin.getDomain(), "tokenSecret-"+myServerName+"-"+myUsername);
  758. } else { tokenSecret = ""; }
  759. api = new TwitterAPI(myUsername, myPassword, apiAddress, "", consumerKey, consumerSecret, token, tokenSecret, apiVersioning, this.apiVersion, getConfigManager().getOptionBool(myPlugin.getDomain(), "autoAt"));
  760. api.setSource("DMDirc");
  761. currentParsers.add(this);
  762. api.addErrorHandler(this);
  763. api.addRawHandler(this);
  764. api.setDebug(getConfigManager().getOptionBool(myPlugin.getDomain(), "debugEnabled"));
  765. getConfigManager().addChangeListener(myPlugin.getDomain(), this);
  766. connected = api.checkConnection();
  767. if (!connected) {
  768. sendPrivateNotice("Unable to connect to "+myServerName+". Disconnecting.");
  769. getCallbackManager().getCallbackType(SocketCloseListener.class).call();
  770. return;
  771. }
  772. final TwitterChannelInfo channel = new TwitterChannelInfo(mainChannelName, this);
  773. synchronized (channels) {
  774. channels.put(mainChannelName, channel);
  775. }
  776. channel.addChannelClient(new TwitterChannelClientInfo(channel, myself));
  777. // Fake 001
  778. getCallbackManager().getCallbackType(ServerReadyListener.class).call();
  779. // Fake 005
  780. getCallbackManager().getCallbackType(NetworkDetectedListener.class).call(this.getNetworkName(), this.getServerSoftware(), this.getServerSoftwareType());
  781. getCallbackManager().getCallbackType(Post005Listener.class).call();
  782. // Fake MOTD
  783. getCallbackManager().getCallbackType(AuthNoticeListener.class).call("Welcome to "+myServerName+".");
  784. getCallbackManager().getCallbackType(MotdStartListener.class).call("- "+myServerName+" Message of the Day -");
  785. getCallbackManager().getCallbackType(MotdLineListener.class).call("- This is an experimental parser, to allow DMDirc to use "+myServerName);
  786. getCallbackManager().getCallbackType(MotdLineListener.class).call("- ");
  787. getCallbackManager().getCallbackType(MotdLineListener.class).call("- Your timeline appears in "+mainChannelName+" (topic is your last status)");
  788. getCallbackManager().getCallbackType(MotdLineListener.class).call("- All messages sent to this channel (or topics set) will cause the status to be set.");
  789. getCallbackManager().getCallbackType(MotdLineListener.class).call("- ");
  790. getCallbackManager().getCallbackType(MotdLineListener.class).call("- Messages can be replied to using /msg &<messageid> <reply>");
  791. getCallbackManager().getCallbackType(MotdLineListener.class).call("- Messages can be retweeted by using /ctcp &<messageid> RT or /ctcp &<messageid> RETWEET");
  792. getCallbackManager().getCallbackType(MotdLineListener.class).call("- ");
  793. getCallbackManager().getCallbackType(MotdEndListener.class).call(false, "End of /MOTD command");
  794. // Fake some more on-connect crap
  795. getCallbackManager().getCallbackType(UserModeDiscoveryListener.class).call(myself, "");
  796. channel.setLocalTopic("No status known.");
  797. this.doJoinChannel(channel);
  798. sendChannelMessage(channel, "Checking to see if we have been authorised to use the account \""+api.getLoginUsername()+"\"...");
  799. if (!api.isAllowed(false)) {
  800. wantAuth = true;
  801. if (api.useOAuth()) {
  802. sendChannelMessage(channel, "Sorry, DMDirc has not been authorised to use the account \""+api.getLoginUsername()+"\"");
  803. sendChannelMessage(channel, "");
  804. sendChannelMessage(channel, "Before you can use DMDirc with "+myServerName+" you need to authorise it.");
  805. sendChannelMessage(channel, "");
  806. sendChannelMessage(channel, "To do this, please visit: "+api.getOAuthURL());
  807. sendChannelMessage(channel, "and then type the PIN here.");
  808. } else {
  809. sendChannelMessage(channel, "Sorry, You did not provide DMDirc with a password for the account \""+api.getLoginUsername()+"\" and the server \""+myServerName+"\" does not support OAuth or is not accepting our key.");
  810. sendChannelMessage(channel, "");
  811. sendChannelMessage(channel, "Before you can use DMDirc with "+myServerName+" you need to provide a password.");
  812. sendChannelMessage(channel, "");
  813. sendChannelMessage(channel, "To do this, please type the password here, or set it correctly in the URL (twitter://" + myUsername + ":your_password@"+myServerName+myAddress.getPath()+").");
  814. }
  815. } else {
  816. sendChannelMessage(channel, "DMDirc has been authorised to use the account \""+api.getLoginUsername()+"\"");
  817. updateTwitterChannel();
  818. }
  819. long lastReplyId = -1;
  820. long lastTimelineId = -1;
  821. long lastDirectMessageId = -1;
  822. if (getConfigManager().getOptionBool(myPlugin.getDomain(), "saveLastIDs")) {
  823. if (getConfigManager().hasOptionString(myPlugin.getDomain(), "lastReplyId-"+myServerName+"-"+myUsername)) {
  824. lastReplyId = TwitterAPI.parseLong(getConfigManager().getOption(myPlugin.getDomain(), "lastReplyId-"+myServerName+"-"+myUsername), -1);
  825. }
  826. if (getConfigManager().hasOptionString(myPlugin.getDomain(), "lastTimelineId-"+myServerName+"-"+myUsername)) {
  827. lastTimelineId = TwitterAPI.parseLong(getConfigManager().getOption(myPlugin.getDomain(), "lastTimelineId-"+myServerName+"-"+myUsername), -1);
  828. }
  829. if (getConfigManager().hasOptionString(myPlugin.getDomain(), "lastDirectMessageId-"+myServerName+"-"+myUsername)) {
  830. lastDirectMessageId = TwitterAPI.parseLong(getConfigManager().getOption(myPlugin.getDomain(), "lastDirectMessageId-"+myServerName+"-"+myUsername), -1);
  831. }
  832. }
  833. boolean first = true; // Used to let used know if there was no new items.
  834. boolean foundItems = false; // Same as above.
  835. int count = 0;
  836. final long pruneCount = 20; // Every 20 loops, clear the status cache of
  837. final long pruneTime = 3600 * 1000 ; // anything older than 1 hour.
  838. while (connected) {
  839. final int startCalls = (wantAuth) ? 0 : api.getUsedCalls();
  840. if (!wantAuth && api.isAllowed()) {
  841. lastQueryTime = System.currentTimeMillis();
  842. final int statusesPerAttempt = Math.min(200, getConfigManager().getOptionInt(myPlugin.getDomain(), "statuscount"));
  843. final List<TwitterStatus> statuses = new ArrayList<TwitterStatus>();
  844. for (TwitterStatus status : api.getReplies(lastReplyId, statusesPerAttempt)) {
  845. statuses.add(status);
  846. if (status.getRetweetId() > lastReplyId) { lastReplyId = status.getRetweetId(); }
  847. }
  848. for (TwitterStatus status : api.getFriendsTimeline(lastTimelineId, statusesPerAttempt)) {
  849. if (!statuses.contains(status)) {
  850. statuses.add(status);
  851. }
  852. // Add new friends that may have been added elsewhere.
  853. if (status.isRetweet()) {
  854. checkUserOnChannel(status.getRetweetUser());
  855. } else {
  856. checkUserOnChannel(status.getUser());
  857. }
  858. if (status.getRetweetId() > lastTimelineId) { lastTimelineId = status.getRetweetId(); }
  859. }
  860. Collections.sort(statuses);
  861. for (TwitterStatus status : statuses) {
  862. foundItems = true;
  863. final ChannelClientInfo cci = channel.getChannelClient(status.getUser().getScreenName());
  864. String message = String.format("%s %c15 &%d", status.getText(), Styliser.CODE_COLOUR, status.getID());
  865. if (status.getReplyTo() > 0) {
  866. message += String.format(" %cin reply to &%d %1$c", Styliser.CODE_ITALIC, status.getReplyTo());
  867. }
  868. if (status.isRetweet()) {
  869. message += String.format(" %c%c15[Retweet by %s]%1$c", Styliser.CODE_BOLD, Styliser.CODE_COLOUR, status.getRetweetUser().getScreenName());
  870. }
  871. final String hostname = status.getUser().getScreenName();
  872. sendChannelMessage(channel, message, cci, hostname);
  873. }
  874. final List<TwitterMessage> directMessages = new ArrayList<TwitterMessage>();
  875. for (TwitterMessage directMessage : api.getDirectMessages(lastDirectMessageId)) {
  876. directMessages.add(directMessage);
  877. if (directMessage.getID() > lastDirectMessageId) { lastDirectMessageId = directMessage.getID(); }
  878. }
  879. if (getConfigManager().getOptionBool(myPlugin.getDomain(), "getSentMessages")) {
  880. for (TwitterMessage directMessage : api.getSentDirectMessages(lastDirectMessageId)) {
  881. directMessages.add(directMessage);
  882. if (directMessage.getID() > lastDirectMessageId) { lastDirectMessageId = directMessage.getID(); }
  883. }
  884. }
  885. Collections.sort(directMessages);
  886. for (TwitterMessage dm : directMessages) {
  887. sendPrivateMessage(dm.getText(), dm.getSenderScreenName(), dm.getTargetScreenName());
  888. }
  889. if (myself != null && myself.getUser() != null) {
  890. checkTopic(channel, myself.getUser().getStatus());
  891. }
  892. if (first) {
  893. first = false;
  894. if (!foundItems) {
  895. sendChannelMessage(channel, "No new items found.");
  896. }
  897. }
  898. }
  899. IdentityManager.getConfigIdentity().setOption(myPlugin.getDomain(), "lastReplyId-"+myServerName+"-"+myUsername, Long.toString(lastReplyId));
  900. IdentityManager.getConfigIdentity().setOption(myPlugin.getDomain(), "lastTimelineId-"+myServerName+"-"+myUsername, Long.toString(lastTimelineId));
  901. IdentityManager.getConfigIdentity().setOption(myPlugin.getDomain(), "lastDirectMessageId-"+myServerName+"-"+myUsername, Long.toString(lastDirectMessageId));
  902. final int apiLimit = getConfigManager().getOptionInt(myPlugin.getDomain(), "apicalls");
  903. final int endCalls = (wantAuth) ? 0 : api.getUsedCalls();
  904. final Long[] apiCalls = (wantAuth) ? new Long[]{0L, 0L, System.currentTimeMillis(), (long)api.getUsedCalls()} : api.getRemainingApiCalls();
  905. doDebug(Debug.apiCalls, "Twitter calls Remaining: "+apiCalls[0]);
  906. // laconica doesn't rate limit, so time to reset is always 0, in this case
  907. // we will assume the time of the next hour.
  908. final Calendar cal = Calendar.getInstance();
  909. cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DATE), cal.get(Calendar.HOUR_OF_DAY)+1, 0, 0) ;
  910. final Long timeLeft = ((apiCalls[2] > 0) ? apiCalls[2] : cal.getTimeInMillis()) - System.currentTimeMillis();
  911. final long sleepTime;
  912. if (wantAuth) {
  913. // When waiting for auth, sleep for less time so that when the
  914. // auth happens, we can quickly start twittering!
  915. sleepTime = 5 * 1000;
  916. } else if (!api.isAllowed()) {
  917. // If we aren't allowed, but aren't waiting for auth, then
  918. // sleep for 1 minute.
  919. sleepTime = 60 * 1000;
  920. } else if (apiCalls[1] == 0L) {
  921. // Twitter has said we have no API Calls in total, so sleep for
  922. // 10 minutes and try again.
  923. // (This will also happen if twitter didn't respond for some reason)
  924. sleepTime = 10 * 60 * 1000;
  925. // Also alert the user.
  926. twitterFail("Unable to communicate with twitter, or no API calls allowed at all, retrying in 10 minutes.");
  927. } else if (api.getUsedCalls() > apiLimit) {
  928. // Sleep for the rest of the hour, we have done too much!
  929. sleepTime = timeLeft;
  930. } else {
  931. // Else work out how many calls we have left.
  932. // Whichever is less between the number of calls we want to make
  933. // and the number of calls twitter is going to allow us to make.
  934. final long callsLeft = Math.min(apiLimit - api.getUsedCalls(), apiCalls[0]);
  935. // How many calls do we make each time?
  936. // If this is less than 0 (If there was a time reset between
  937. // calculating the start and end calls used) then assume 3.
  938. final long callsPerTime = (endCalls - startCalls) > 0 ? (endCalls - startCalls) : 3;
  939. doDebug(Debug.apiCalls, "\tCalls Remaining: "+callsLeft);
  940. doDebug(Debug.apiCalls, "\tCalls per time: "+callsPerTime);
  941. // And divide this by the number of calls we make each time to
  942. // see how many times we have to sleep this hour.
  943. final long sleepsRequired = callsLeft / callsPerTime;
  944. doDebug(Debug.sleepTime, "\tSleeps Required: "+sleepsRequired);
  945. doDebug(Debug.sleepTime, "\tTime Left: "+timeLeft);
  946. // Then finally discover how long we need to sleep for.
  947. sleepTime = (sleepsRequired > 0) ? timeLeft / sleepsRequired : timeLeft;
  948. }
  949. doDebug(Debug.sleepTime, "Sleeping for: "+sleepTime);
  950. // Sleep for sleep time,
  951. // If we have a negative sleep time, use 5 minutes.
  952. try { Thread.sleep((sleepTime > 0) ? sleepTime : 5 * 60 * 1000); } catch (InterruptedException ex) { }
  953. if (++count > pruneCount) {
  954. api.pruneStatusCache(System.currentTimeMillis() - pruneTime);
  955. }
  956. }
  957. }
  958. /**
  959. * Reset the state of the parser.
  960. */
  961. private void resetState() {
  962. resetState(false);
  963. }
  964. /**
  965. * Reset the state of the parser.
  966. *
  967. * @param simpleMyself Don't check the config when setting myself if true.
  968. */
  969. private void resetState(final boolean simpleMyself) {
  970. connected = false;
  971. synchronized (channels) {
  972. channels.clear();
  973. }
  974. clients.clear();
  975. if (!simpleMyself && getConfigManager().getOptionBool(myPlugin.getDomain(), "autoAt")) {
  976. myself = new TwitterClientInfo("@" + myUsername, this);
  977. } else {
  978. myself = new TwitterClientInfo(myUsername, this);
  979. }
  980. }
  981. /**
  982. * Get the Twitter API Object
  983. *
  984. * @return The Twitter API Object
  985. */
  986. public TwitterAPI getApi() {
  987. return api;
  988. }
  989. /** {@inheritDoc} */
  990. @Override
  991. public StringConverter getStringConverter() {
  992. return myStringConverter;
  993. }
  994. /** {@inheritDoc} */
  995. @Override
  996. public void setIgnoreList(final IgnoreList ignoreList) {
  997. myIgnoreList = ignoreList;
  998. }
  999. /** {@inheritDoc} */
  1000. @Override
  1001. public IgnoreList getIgnoreList() {
  1002. return myIgnoreList;
  1003. }
  1004. /**
  1005. * Set the twitter status.
  1006. *
  1007. * @param message Status to use.
  1008. * @return True if status was updated, else false.
  1009. */
  1010. public boolean setStatus(final String message) {
  1011. return setStatus(message, -1);
  1012. }
  1013. /**
  1014. * Set the twitter status.
  1015. *
  1016. * @param message Status to use.
  1017. * @param id
  1018. * @return True if status was updated, else false.
  1019. */
  1020. private boolean setStatus(final String message, final long id) {
  1021. final StringBuffer newStatus = new StringBuffer(message);
  1022. final TwitterChannelInfo channel = (TwitterChannelInfo) this.getChannel(mainChannelName);
  1023. if (channel != null && getConfigManager().getOptionBool(myPlugin.getDomain(), "replaceOpeningNickname")) {
  1024. final String[] bits = message.split(" ");
  1025. if (bits[0].charAt(bits[0].length() - 1) == ':') {
  1026. final String name = bits[0].substring(0, bits[0].length() - 1);
  1027. final ChannelClientInfo cci = channel.getChannelClient(name);
  1028. if (cci != null) {
  1029. if (cci.getClient().getNickname().charAt(0) != '@') {
  1030. bits[0] = "@" + cci.getClient().getNickname();
  1031. } else {
  1032. bits[0] = cci.getClient().getNickname();
  1033. }
  1034. newStatus.setLength(0);
  1035. for (String bit : bits) {
  1036. if (newStatus.length() > 0) { newStatus.append(" "); }
  1037. newStatus.append(bit);
  1038. }
  1039. }
  1040. }
  1041. }
  1042. if (api.setStatus(newStatus.toString(), id)) {
  1043. checkTopic(channel, myself.getUser().getStatus());
  1044. return true;
  1045. }
  1046. return false;
  1047. }
  1048. /**
  1049. * Rename the given client from the given name.
  1050. *
  1051. * @param client Client to rename
  1052. * @param old Old nickname
  1053. */
  1054. void renameClient(final TwitterClientInfo client, final String old) {
  1055. clients.remove(old.toLowerCase());
  1056. clients.put(client.getNickname().toLowerCase(), client);
  1057. getCallbackManager().getCallbackType(NickChangeListener.class).call(client, old);
  1058. }
  1059. @Override
  1060. public int getMaxLength() {
  1061. return 140;
  1062. }
  1063. /**
  1064. * Make the core think a channel was joined.
  1065. *
  1066. * @param channel Channel to join.
  1067. */
  1068. private void doJoinChannel(final TwitterChannelInfo channel) {
  1069. // Fake Join Channel
  1070. getCallbackManager().getCallbackType(ChannelSelfJoinListener.class).call(channel);
  1071. getCallbackManager().getCallbackType(ChannelTopicListener.class).call(channel, true);
  1072. getCallbackManager().getCallbackType(ChannelNamesListener.class).call(channel);
  1073. getCallbackManager().getCallbackType(ChannelModeChangeListener.class).call(channel, null, "", "");
  1074. }
  1075. /**
  1076. * Update the users and topic of the main channel.
  1077. */
  1078. private void updateTwitterChannel() {
  1079. final TwitterChannelInfo channel = (TwitterChannelInfo) getChannel(mainChannelName);
  1080. checkTopic(channel, myself.getUser().getStatus());
  1081. channel.clearChannelClients();
  1082. channel.addChannelClient(new TwitterChannelClientInfo(channel, myself));
  1083. for (TwitterUser user : api.getFriends()) {
  1084. final TwitterClientInfo ci = new TwitterClientInfo(user.getScreenName(), this);
  1085. clients.put(ci.getNickname().toLowerCase(), ci);
  1086. final TwitterChannelClientInfo cci = new TwitterChannelClientInfo(channel, ci);
  1087. channel.addChannelClient(cci);
  1088. }
  1089. api.getFollowers();
  1090. getCallbackManager().getCallbackType(ChannelNamesListener.class).call(channel);
  1091. }
  1092. /**
  1093. * Check if the topic in the given channel has been changed, and if it has
  1094. * fire the callback.
  1095. *
  1096. * @param channel channel to check.
  1097. * @param status Status to use to update the topic with.
  1098. */
  1099. private void checkTopic(final TwitterChannelInfo channel, final TwitterStatus status) {
  1100. if (channel == null || status == null) { return; }
  1101. final String oldStatus = channel.getTopic();
  1102. final String newStatus = (status.isRetweet()) ? status.getRetweetText() : status.getText();
  1103. if (!newStatus.equalsIgnoreCase(oldStatus)) {
  1104. channel.setTopicSetter(status.getUser().getScreenName());
  1105. channel.setTopicTime(status.getTime() / 1000);
  1106. channel.setLocalTopic(newStatus);
  1107. getCallbackManager().getCallbackType(ChannelTopicListener.class).call(channel, false);
  1108. }
  1109. }
  1110. /** {@inheritDoc} */
  1111. @Override
  1112. public void handleTwitterError(final TwitterAPI api, final Throwable t, final String source, final String twitterInput, final String twitterOutput, final String message) {
  1113. final boolean debug = getConfigManager().getOptionBool(myPlugin.getDomain(), "debugEnabled");
  1114. final boolean hide500Errors = !debug && getConfigManager().getOptionBool(myPlugin.getDomain(), "hide500Errors");
  1115. if (hide500Errors && message.matches("^\\(50[0-9]\\).*")) { return; }
  1116. try {
  1117. if (!message.isEmpty()) {
  1118. twitterFail("Recieved an error from twitter: " + message + (debug ? " [" + source + "]" : ""));
  1119. } else if (debug) {
  1120. twitterFail("Recieved an error: " + source);
  1121. }
  1122. if (t != null) {
  1123. doDebug(Debug.twitterError, t.getClass().getSimpleName()+": "+t+" -> "+t.getMessage());
  1124. }
  1125. // And give more information:
  1126. doDebug(Debug.twitterErrorMore, "Source: "+source);
  1127. doDebug(Debug.twitterErrorMore, "Input: "+twitterInput);
  1128. doDebug(Debug.twitterErrorMore, "Output: ");
  1129. for (String out : twitterOutput.split("\n")) {
  1130. doDebug(Debug.twitterErrorMore, " "+out);
  1131. }
  1132. doDebug(Debug.twitterErrorMore, "");
  1133. doDebug(Debug.twitterErrorMore, "Exception:");
  1134. // Hax the error manager to get a nice String[] representing the stack trace and output it.
  1135. try {
  1136. final Method gt = ErrorManager.class.getDeclaredMethod("getTrace", Throwable.class);
  1137. gt.setAccessible(true);
  1138. final String[] trace = (String[]) gt.invoke(ErrorManager.getErrorManager(), t);
  1139. for (String out : trace) {
  1140. doDebug(Debug.twitterErrorMore, " "+out);
  1141. }
  1142. } catch (NoSuchMethodException ex) {
  1143. doDebug(Debug.twitterErrorMore, " ... Unable to get StackTrace (nsme: "+ex+")");
  1144. } catch (SecurityException ex) {
  1145. doDebug(Debug.twitterErrorMore, " ... Unable to get StackTrace (se: "+ex+")");
  1146. } catch (IllegalAccessException ex) {
  1147. doDebug(Debug.twitterErrorMore, " ... Unable to get StackTrace (iae: "+ex+")");
  1148. } catch (IllegalArgumentException ex) {
  1149. doDebug(Debug.twitterErrorMore, " ... Unable to get StackTrace (iae2: "+ex+")");
  1150. } catch (InvocationTargetException ex) {
  1151. doDebug(Debug.twitterErrorMore, " ... Unable to get StackTrace (ite: "+ex+")");
  1152. }
  1153. doDebug(Debug.twitterErrorMore, "==================================");
  1154. } catch (Throwable t2) {
  1155. doDebug(Debug.twitterError, "wtf? (See Console for stack trace) "+t2);
  1156. t2.printStackTrace();
  1157. }
  1158. }
  1159. /**
  1160. * This method will send data to the NumericListener and the DataInListener
  1161. *
  1162. * @param numeric Numeric
  1163. * @param token Tokenised Representation.
  1164. */
  1165. private void sendNumericOutput(final int numeric, final String[] token) {
  1166. getCallbackManager().getCallbackType(NumericListener.class).call(numeric, token);
  1167. final StringBuffer output = new StringBuffer();
  1168. for (String bit : token) {
  1169. output.append(" ");
  1170. output.append(bit);
  1171. }
  1172. getCallbackManager().getCallbackType(DataInListener.class).call(output.toString().trim());
  1173. }
  1174. /** {@inheritDoc} */
  1175. @Override
  1176. public void handleRawTwitterInput(final TwitterAPI api, final String data) {
  1177. doDebug(Debug.dataIn, "-------------------------");
  1178. getCallbackManager().getCallbackType(DataInListener.class).call("-------------------------");
  1179. for (String line : data.split("\n")) {
  1180. doDebug(Debug.dataIn, line);
  1181. getCallbackManager().getCallbackType(DataInListener.class).call(line);
  1182. }
  1183. }
  1184. /** {@inheritDoc} */
  1185. @Override
  1186. public void handleRawTwitterOutput(final TwitterAPI api, final String data) {
  1187. doDebug(Debug.dataOut, "-------------------------");
  1188. getCallbackManager().getCallbackType(DataOutListener.class).call("-------------------------", true);
  1189. for (String line : data.split("\n")) {
  1190. doDebug(Debug.dataOut, line);
  1191. getCallbackManager().getCallbackType(DataOutListener.class).call(line, true);
  1192. }
  1193. }
  1194. /**
  1195. * Let the user know twitter failed in some way.
  1196. *
  1197. * @param message Message to send to the user
  1198. */
  1199. private void twitterFail(final String message) {
  1200. if (Math.random() <= 0.10) { showFailWhale(); }
  1201. doDebug(Debug.twitterError, message);
  1202. sendPrivateNotice(message);
  1203. }
  1204. /**
  1205. * Returns the TwitterPlugin that owns us.
  1206. *
  1207. * @return The TwitterPlugin that owns us.
  1208. */
  1209. public TwitterPlugin getMyPlugin() {
  1210. return myPlugin;
  1211. }
  1212. /** {@inheritDoc} */
  1213. @Override
  1214. public void configChanged(final String domain, final String key) {
  1215. if (domain.equalsIgnoreCase(myPlugin.getDomain())) {
  1216. if (key.equalsIgnoreCase("debugEnabled")) {
  1217. api.setDebug(getConfigManager().getOptionBool(myPlugin.getDomain(), "debugEnabled"));
  1218. } else if (key.equalsIgnoreCase("autoAt")) {
  1219. sendPrivateNotice("'autoAt' setting was changed, reconnect needed.");
  1220. disconnect("'autoAt' setting was changed, reconnect needed.");
  1221. }
  1222. }
  1223. }
  1224. /**
  1225. * Get the config manager for this parser instance.
  1226. *
  1227. * @return the ConfigManager for this parser.
  1228. */
  1229. protected ConfigManager getConfigManager() {
  1230. if (myConfigManager == null) {
  1231. myConfigManager = new ConfigManager(myAddress.getScheme(), getServerSoftwareType(), getNetworkName(), getServerName());
  1232. }
  1233. return myConfigManager;
  1234. }
  1235. }