PageRenderTime 52ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/main/com/jdbernard/twitter/TwitterCLI.groovy

http://github.com/jdbernard/gritter
Groovy | 1127 lines | 648 code | 203 blank | 276 comment | 82 complexity | 46e921b49fe2ad9601473696a7e01362 MD5 | raw file
  1. /**
  2. * # TwitterCLI
  3. * @author Jonathan Bernard (jdbernard@gmail.com)
  4. * @org jdbernard.com/twitter/TwitterCLI
  5. * @copyright 2010-2012 Jonathan Bernard
  6. */
  7. package com.jdbernard.twitter
  8. import com.martiansoftware.nailgun.NGContext
  9. import org.slf4j.Logger
  10. import org.slf4j.LoggerFactory
  11. import twitter4j.Paging
  12. import twitter4j.Status
  13. import twitter4j.Twitter
  14. import twitter4j.TwitterFactory
  15. import twitter4j.conf.Configuration
  16. import twitter4j.conf.PropertyConfiguration
  17. /**
  18. * TwitterCLI is a command-line interface (CLI) to the [Twitter API].
  19. *
  20. * [Twitter API]: https://dev.twitter.com/docs
  21. */
  22. public class TwitterCLI {
  23. private static String EOL = System.getProperty("line.separator")
  24. private static TwitterCLI nailgunInst
  25. private Twitter twitter
  26. private Scanner stdin
  27. private Map colors = [:]
  28. private int terminalWidth
  29. private boolean colored
  30. private boolean printStatusTimestamps
  31. private boolean strict
  32. private boolean warnings
  33. private static final VERSION = 1.0
  34. private Logger log = LoggerFactory.getLogger(getClass())
  35. /// ## Main Methods ##
  36. /// ------------------
  37. /**
  38. * #### main
  39. * This is the main entry-point to the client.
  40. * @org jdbernard.com/twitter/TwitterCLI/main
  41. */
  42. public static void main(String[] args) {
  43. TwitterCLI inst = new TwitterCLI(new File(System.getProperty("user.home"),
  44. ".gritterrc"))
  45. // trim the last argumnet, as not all cli's are well-behaved
  46. args[-1] = args[-1].trim()
  47. inst.run((args as List) as LinkedList) }
  48. /**
  49. * #### nailMain
  50. * This is the entry point when the client is called from a [Nailgun]
  51. * instance.
  52. *
  53. * [Nailgun]: http://www.martiansoftware.com/nailgun/
  54. * @org jdbernard.com/twitter/TwitterCLI/nailMain
  55. */
  56. public static void nailMain(NGContext context) {
  57. if (nailgunInst == null)
  58. nailgunInst = new TwitterCLI(new File(
  59. System.getProperty("user.home"), ".gritterrc"))
  60. else
  61. nailgunInst.stdin = new Scanner(context.in)
  62. // trim the last argumnet, as not all cli's are well-behaved
  63. context.args[-1] = context.args[-1].trim()
  64. nailgunInst.run((context.args as List) as LinkedList) }
  65. /// ## Configuration and Setup ##
  66. /// -----------------------------
  67. /**
  68. * #### reconfigure
  69. * Reload the configuration and pass the rest of the arguments back to be
  70. * processed.
  71. * @org jdbernard.com/twitter/TwitterCLI/reconfigure
  72. */
  73. public static void reconfigure(LinkedList args) {
  74. if (nailgunInst == null) main(args as String[])
  75. else {
  76. nailgunInst = null
  77. nailgunInst = new TwitterCLI(new File(
  78. System.getProperty("user.home"), ".gritterrc"))
  79. nailgunInst.run(args) }}
  80. /**
  81. * #### TwitterCLI
  82. * Initialize this instance based on the given configuration file.
  83. */
  84. public TwitterCLI(File propFile) {
  85. // load the configuration
  86. Properties cfg = new Properties()
  87. propFile.withInputStream { is -> cfg.load(is) }
  88. // create a twitter instance
  89. twitter = (new TwitterFactory(new PropertyConfiguration(cfg))).getInstance()
  90. // configure the colors
  91. colors.author = new ConsoleColor(cfg.getProperty("colors.author", "CYAN:false"))
  92. colors.mentioned = new ConsoleColor(cfg.getProperty("colors.mentioned", "GREEN:false"))
  93. colors.error = new ConsoleColor(cfg.getProperty("colors.error", "RED:true"))
  94. colors.option = new ConsoleColor(cfg.getProperty("colors.option", "YELLOW:true"))
  95. colors.even = new ConsoleColor(cfg.getProperty("colors.even", "WHITE"))
  96. colors.odd = new ConsoleColor(cfg.getProperty("colors.odd", "YELLOW"))
  97. // configure the terminal width
  98. terminalWidth = (System.getenv().COLUMNS ?: cfg.terminalWidth ?: 79) as int
  99. colored = (cfg.colored ?: 'true') as boolean
  100. strict = (cfg.strict ?: 'false') as boolean
  101. warnings = (cfg.warnings ?: 'true') as boolean
  102. printStatusTimestamps = (cfg."timeline.printTimestamps" ?: 'true') as boolean
  103. stdin = new Scanner(System.in)
  104. }
  105. /// ## Parsing Functions ##
  106. /// -----------------------
  107. /**
  108. | #### run
  109. | Main entry to the command line parsing system. In general, the parsing
  110. | system (this method and the others dedicated to parsing arguments)
  111. | follow some common conventions:
  112. |
  113. | 1. An argument is either considered a *command* or a *parameter*.
  114. | 2. Arguments are parsed in order.
  115. | 3. A command can have aliases to make invokation read naturally.
  116. | 4. A command may expect further refining commands, one of which
  117. | will be the default. So if a command has possible refining
  118. | commands, but the next argument does not match any of them,
  119. | the default will be assumed and the current argument will not
  120. | be comsumed, but passed on to the refining command.
  121. |
  122. | For example:
  123. |
  124. | gritter set colored off show mine
  125. |
  126. | is parsed:
  127. |
  128. | * the first argument is expected to be a command
  129. | * the `set` command consumes two arguments, expecting both to
  130. | be parameters
  131. | * the set command is finished, but there are still arguments, so the
  132. | parsing process starts again, expecting the next argument to be a
  133. | command.
  134. | * the `show` command consumes one argument and expects a command
  135. | * `mine` does not match any of the possible refinements for `show` so
  136. | the default command, `timeline`, is assumed.
  137. | * the `show timeline` command consumes one argument, expecting a command
  138. | * The `show timeline mine` command executes
  139. | * No more arguments remain, so execution terminates.
  140. |
  141. | Recognized top-level commands are:
  142. |
  143. | `delete`
  144. | : Delete a post, status, list membership, etc.
  145. | *aliases: `destroy`, `remove`.*
  146. |
  147. | `get`
  148. | : Display a list, timeline, list membership, etc.
  149. | *aliases: `show`.*
  150. |
  151. | `help`
  152. | : Display help for commands.
  153. |
  154. | `post`
  155. | : Post a new status, add a new list subscription, etc.
  156. | *aliases: `add`, `create`.*
  157. |
  158. | `reconfigure`
  159. | : Cause the tool to reload its configuration file.
  160. |
  161. | `set`
  162. | : Set a configurable value at runtime.
  163. |
  164. | `@param args A {@link java.util.LinkedList} of arguments to parse.`
  165. */
  166. public void run(LinkedList args) {
  167. if (args.size() < 1) printUsage()
  168. log.debug("argument list: {}", args)
  169. while (args.peek()) {
  170. def command = args.poll()
  171. switch (command.toLowerCase()) {
  172. case ~/delete|destroy|remove/: delete(args); break
  173. case ~/get|show/: get(args); break // get|show
  174. case ~/help/: help(args); break // help
  175. case ~/post|add|create/: post(args); break // post|add|create
  176. case ~/reconfigure/: reconfigure(args); break // reconfigure
  177. case ~/set/: set(args); break // set
  178. default: // fallthrough
  179. if (strict) {
  180. log.error(color("Unrecognized command: '$command'",
  181. colors.error)) }
  182. else {
  183. if (warnings) {
  184. println "Command '$command' unrecognized: " +
  185. "assuming this is a parameter to 'show'" }
  186. args.addFirst(command)
  187. get(args) }}}}
  188. /// ### `DELETE` Subparsing.
  189. /**
  190. | #### delete
  191. | Parse a `delete` command. Valid options are:
  192. |
  193. | `status`
  194. | : Destroy a status given a status id. *This is the default command.*
  195. |
  196. | `list`
  197. | : Delete a list, remove list members, etc.
  198. |
  199. | `@param args A {@link java.util.LinkedList} of arguments.`
  200. */
  201. public void delete(LinkedList args) {
  202. def option = args.poll()
  203. log.debug("Processing a 'delete' command, option = {}.", option)
  204. switch(option) {
  205. case "status": deleteStatus(args); break
  206. case "list": deleteList(args); break
  207. default: args.addFirst(option)
  208. deleteStatus(args) }}
  209. /**
  210. | #### deleteList
  211. | Parse a `delete list` command. Valid options are:
  212. |
  213. | `member`
  214. | : Remove a member from a list.
  215. |
  216. | `subscription`
  217. | : Unsubcribe from a given list.
  218. |
  219. | *list-reference*
  220. | : Delete the list specified by the reference.
  221. |
  222. | `@param args A {@link java.util.LinkedList} of arguments.`
  223. */
  224. public void deleteList(LinkedList args) {
  225. def option = args.poll()
  226. log.debug("Processing a 'delete list' command, option = {}.", option)
  227. switch(option) {
  228. case "member": deleteListMember(args); break
  229. case "subscription": deleteListSubscription(args); break
  230. default: args.addFirst(option)
  231. doDeleteList(args) }}
  232. /// ### `GET/SHOW` Subparsing
  233. /**
  234. | #### get
  235. | Parse a `get` command. Valid options are:
  236. |
  237. | `list`
  238. | : Show a list timeline, members, subs, etc.
  239. |
  240. | `lists`
  241. | : Show lists all lists owned by a given user.
  242. |
  243. | `subscriptions`
  244. | : Show all of the lists a given user is subcribed to.
  245. |
  246. | `timeline`
  247. | : Show a timeline of tweets. *This is the default command.*
  248. |
  249. | `user`
  250. | : Show information about a given users.
  251. |
  252. | `@param args A {@link java.util.LinkedList} of arguments.`
  253. */
  254. public void get(LinkedList args) {
  255. def option = args.poll()
  256. log.debug("Processing a 'get' command, option = {}.", option)
  257. switch(option) {
  258. case "list": showList(args); break
  259. case "lists": showLists(args); break
  260. case ~/subs.*/: showSubscriptions(args); break
  261. case "timeline": showTimeline(args); break
  262. case "user": showUser(args); break
  263. default: args.addFirst(option)
  264. showTimeline(args) }}
  265. /**
  266. | #### showList
  267. | Parse a `show list` command. Valid options are:
  268. |
  269. | `members`
  270. | : Show the members of a given list. This is the list of users who's
  271. | tweets comprise the list.
  272. |
  273. | `subscribers`
  274. | : Show the subscribers of a given list. This is the list of users who
  275. | are see the list.
  276. |
  277. | `subscriptions`
  278. | : Show all of the lists a given user is subscribed to.
  279. |
  280. | *list-reference*
  281. | : Show the timeline for a given list.
  282. |
  283. | `@param args a {@link java.util.LinkedList} of arguments.`
  284. */
  285. public void showList(LinkedList args) {
  286. def option = args.poll()
  287. log.debug("Processing a 'show list' command, option = '{}'", option)
  288. switch(option) {
  289. case "members": showListMembers(args); break
  290. case "subscribers": showListSubscribers(args); break
  291. case "subscriptions": showListSubscriptions(args); break
  292. default: args.addFirst(option)
  293. showListTimeline(args) }}
  294. /**
  295. | #### showLists
  296. | Parse a `show lists` command. `show lists` consumes at most one
  297. | argument, representing a user reference. It shows all of the lists
  298. | owned by the given user. If no user reference is given `show lists`
  299. | shows the lists owned by the currently logged in user.
  300. |
  301. | `@param args a {@link java.util.LinkedList} of arguments.`
  302. */
  303. public void showLists(LinkedList args) {
  304. def user = args.poll()
  305. log.debug("Processing a 'show lists' command, user = '{}'", user)
  306. if (!user) user = twitter.screenName
  307. // TODO paging
  308. printLists(twitter.getUserLists(user, -1)) }
  309. /**
  310. | #### showListMembers
  311. | Parse a `show list members` command. `show list members` consumes
  312. | one argument, a reference to the list in question.
  313. |
  314. | `@param args a {@util java.util.LinkedList} of arguments.`
  315. */
  316. public void showListMembers(LinkedList args) {
  317. def listRef = parseListReference(args)
  318. log.debug("Processing a 'show list members' command, list = '{}'",
  319. listRef)
  320. if (!listRef) {
  321. println color("show list members", colors.option) +
  322. color(" command requires a list reference in the form of " +
  323. "user/list: ", colors.error) +
  324. "gritter show list members <user/list>"
  325. return }
  326. def userList = twitter.getUserListMembers(listRef.username,
  327. listRef.listId, -1)
  328. // TODO paging
  329. printUserList(userList) }
  330. /**
  331. | #### showListSubscribers
  332. | Parse a `show list subscribers` command. `show list subscribers`
  333. | consumes one argument, a reference to the list in question.
  334. |
  335. | `@param args a {@util java.util.LinkedList} of arguments.`
  336. */
  337. public void showListSubscribers(LinkedList args) {
  338. def listRef = parseListReference(args)
  339. log.debug("Processing a 'show list subscribers' command, list = '{}'",
  340. listRef)
  341. if (!listRef) {
  342. println color("show list subscribers", colors.option) +
  343. color(" command requires a list reference in the form of " +
  344. "user/list: ", colors.error) +
  345. "gritter show list subscribers <user/list>"
  346. return }
  347. // TODO: paging
  348. printUserList(twitter.getUserListSubscribers(
  349. listRef.username, listRef.listId, -1)) }
  350. /**
  351. | #### showListSubscriptions
  352. | Parse a `show list subscriptions` command. `show list subscriptions`
  353. | consumes one argument, a reference to the list in question.
  354. |
  355. | `@param args a {@util java.util.LinkedList} of arguments.`
  356. */
  357. public void showListSubscriptions(LinkedList args) {
  358. def user = args.poll()
  359. log.debug("Processing a 'show list subscriptions' command, list = '{}'",
  360. listRef)
  361. if (!user) user = twitter.screenName
  362. // TODO: paging
  363. printLists(twitter.getUserListSubscriptions(user, -1)) }
  364. /**
  365. | #### showListTimeline
  366. | Parse a `show list timeline` command. `show list timeline` consumes one
  367. | argument, a reference to a user's list.
  368. |
  369. | `@param args a {@util java.util.LinkedList} of arguments.`
  370. */
  371. public void showListTimeline(LinkedList args) {
  372. if (args.size() < 1) {
  373. println color("show list", colors.option) +
  374. color(" command requires a list reference in the form of " +
  375. "user/list: ", colors.error) +
  376. "gritter show list <user/list>"
  377. return }
  378. def listRef = parseListReference(args)
  379. log.debug("Showing a list timeline, list = '{}'", listRef)
  380. if (listRef.listId == -1) {
  381. println color("show list", colors.option) +
  382. color(" command requires a list reference in the form of " +
  383. "user/list: ", colors.error) +
  384. "gritter show list <user/list>"
  385. return }
  386. // TODO: paging
  387. printTimeline(twitter.getUserListStatuses(
  388. listRef.username, listRef.listId, new Paging())) }
  389. /**
  390. | #### showTimeline
  391. |
  392. */
  393. public void showTimeline(LinkedList args) {
  394. String timeline = args.poll() ?: "home"
  395. log.debug("Processing a 'show timeline' command, timeline = '{}'",
  396. timeline)
  397. switch (timeline) {
  398. case "friends": printTimeline(twitter.friendsTimeline); break
  399. case "home": printTimeline(twitter.homeTimeline); break
  400. case "mine": printTimeline(twitter.userTimeline); break
  401. case "public": printTimeline(twitter.publicTimeline); break
  402. case "user":
  403. String user = args.poll()
  404. if (user) {
  405. if (user.isNumber())
  406. printTimeline(twitter.getUserTimeline(user as int))
  407. else printTimeline(twitter.getUserTimeline(user)) }
  408. else println color("No user specified.", colors.error)
  409. break;
  410. default:
  411. println color("Unknown timeline: ", colors.error) +
  412. color(timeline, colors.option)
  413. break; }}
  414. /**
  415. * #### showUser
  416. */
  417. public void showUser(LinkedList args) {
  418. def user = args.poll()
  419. log.debug("Processing a 'show user' command, user = '{}'", user)
  420. println color("show user", colors.option) +
  421. color(" is not yet implemented.", colors.error) }
  422. /**
  423. * #### createList
  424. */
  425. public void createList(LinkedList args) {
  426. def option = args.poll()
  427. switch(option) {
  428. case "member": addListMember(args); break
  429. case "subscription": addListSubscription(args); break
  430. default: args.addFirst(option)
  431. createNewList(args); break }}
  432. /**
  433. * #### post
  434. */
  435. public void post(LinkedList args) {
  436. def option = args.poll()
  437. log.debug("Processing a 'post' command: option = '{}'", option)
  438. if (!option) {
  439. println color("post", colors.option) +
  440. color(" command requires at least two parameters: ",
  441. colors.error) + "gritter post <status|retweet|list> " +
  442. "<options>..."
  443. return }
  444. switch (option) {
  445. case "status": postStatus(args.poll()); break
  446. case "retweet": retweetStatus(args.poll()); break
  447. case "list": createList(args); break
  448. default: postStatus(option) }}
  449. /**
  450. * #### set
  451. */
  452. public void set(LinkedList args) {
  453. def option = args.poll()
  454. def value = args.poll()
  455. log.debug("Processing a 'set' command: option = '{}', value = '{}'",
  456. option, value)
  457. if (!value) { // note: if option is null, value is null
  458. println color("set", colors.option) +
  459. color(" command requires two options: ", colors.error) +
  460. "gritter set <param> <value>"
  461. return }
  462. switch (option) {
  463. case "terminalWidth": terminalWidth = value as int; break
  464. case "colored": colored = value.toLowerCase() ==~ /true|t|on|yes|y/
  465. break
  466. default:
  467. println color("No property named ", colors.error) +
  468. color(option, colors.option) +
  469. color(" exists.", colors.error) }}
  470. /// ## Worker Functions ##
  471. /// ----------------------
  472. /**
  473. * #### deleteListMember
  474. */
  475. public void deleteListMember(LinkedList args) {
  476. def listRef = parseListReference(args)
  477. def user = args.poll()
  478. log.debug("Deleting a member from a list: list='{}', user='{}'",
  479. listRef, user)
  480. if (!user) {
  481. println color("delete list member", colors.option) +
  482. color(" requires two parameters: ", colors.error) +
  483. "gritter delete list member <list-ref> <user>"
  484. return }
  485. // look up the user id if neccessary
  486. if (user.isLong()) user = user as long
  487. else user = twitter.showUser(user).id
  488. twitter.deleteUserListMember(listRef.listId, user)
  489. // TODO: error checking? print list name?
  490. println "Deleted " + color("@$user", colors.mentioned) +
  491. " from your list." }
  492. /**
  493. * #### deleteListSubscription
  494. */
  495. public void deleteListSubscription(LinkedList args) {
  496. def listRef = parseListReference(args)
  497. log.debug("Unsubscribing from a list: listRef='{}', user='{}'",
  498. listRef, user)
  499. if (!listRef) {
  500. println color("delete list subscription", colors.option) +
  501. color(" requires a list reference: ", colors.error) +
  502. "gritter delete list subscription <list-ref>"
  503. return }
  504. twitter.unsubscribeUserList(listRef.username, listRef.listId)
  505. // TODO: error checking?
  506. println "Unsubscribed from list: " + color(listRef.toString(),
  507. colors.option) }
  508. /**
  509. * #### doDeleteList
  510. */
  511. public void doDeleteList(LinkedList args) {
  512. def listRef = parseListReference(args)
  513. log.debug("Destroying a list: listRef='{}'", listRef)
  514. if (!listRef) {
  515. println color("destroy list", colors.option) +
  516. color(" requries a list reference: ", colors.error) +
  517. "gritter destroy list <list-ref>"
  518. return }
  519. println "Really destroy list '" + color(listRef.toString(),
  520. colors.option) + "' ?"
  521. if (stdin.nextLine() ==~ /yes|y|true|t/) {
  522. try {
  523. twitter.destroyUserList(listRef.listId)
  524. println "List destroyed." }
  525. catch (Exception e) {
  526. println "An error occurred trying to delete the list: '" +
  527. e.localizedMessage
  528. log.error("Error destroying list:", e) }}
  529. else println "Destroy list canceled." }
  530. /**
  531. * #### deleteStatus
  532. */
  533. public void deleteStatus(LinkedList args) {
  534. def statusId = args.poll()
  535. log.debug("Destroying a status: id='{}'", statusId)
  536. if (!statusId || !statusId.isLong()) {
  537. println color("destroy status", colors.option) +
  538. color(" requires a status id: ", colors.error) +
  539. "gritter delete status <status-id>"
  540. return }
  541. statusId = statusId as long
  542. println "Really destroy status '" + color(statusId.toString(),
  543. colors.option) + "' ?"
  544. if (stdin.nextLine() ==~ /yes|y|true|t/) {
  545. try {
  546. twitter.destroyStatus(statusId)
  547. println "Status destroyed." }
  548. catch (Exception e) {
  549. println "An error occurred trying to destroy the status: '" +
  550. e.localizedMessage
  551. log.error("Error destroying status:", e) }}
  552. else println "Destroy status canceled." }
  553. /**
  554. * #### printLists
  555. */
  556. public void printLists(def lists) {
  557. int colSize = 0
  558. // fins largest indentation needed
  559. lists.each { list ->
  560. def curColSize = list.user.screenName.length() +
  561. list.slug.length() + list.id.toString().length() + 3
  562. colSize = Math.max(colSize, curColSize)
  563. //println colSize //TODO, fix column alignment
  564. }
  565. lists.each { list ->
  566. //println colSize
  567. def col1 = color("@${list.user.screenName}", colors.author) + "/" +
  568. color("${list.slug} (${list.id})", colors.option)
  569. println col1.padLeft(colSize) + ": ${list.memberCount} members " +
  570. "and ${list.subscriberCount} subscribers"
  571. println wrapToWidth(list.description, terminalWidth,
  572. "".padLeft(8), "")
  573. //println col1.length()
  574. }
  575. }
  576. public void printUserList(def users) {
  577. int colSize = 0
  578. colSize = users.inject(0) {
  579. curMax, user -> Math.max(curMax, user.id.toString().length()) }
  580. users.each { user ->
  581. println "${user.id.toString().padLeft(colSize)} - " +
  582. color(user.screenName, colors.author) + ": ${user.name}"
  583. }
  584. }
  585. public void printTimeline(def timeline) {
  586. log.debug("Printing a timeline: {}", timeline)
  587. Map formatOptions = [:]
  588. formatOptions.authorLength = 0
  589. timeline.each { status ->
  590. if (status.user.screenName.length() > formatOptions.authorLength)
  591. formatOptions.authorLength = status.user.screenName.length()
  592. }
  593. formatOptions.indent = "".padLeft(formatOptions.authorLength + 2)
  594. timeline.eachWithIndex { status, rowNum ->
  595. formatOptions.rowNum = rowNum
  596. println formatStatus(status, formatOptions)
  597. }
  598. }
  599. public void postStatus(String status) {
  600. log.debug("Posting a status: '{}'", status)
  601. if (!status) {
  602. println color("post status ", colors.option) +
  603. color("command requires one option: ", colors.error) +
  604. "gritter post status <status>"
  605. return
  606. }
  607. if (status.length() > 140) {
  608. println color("Status exceeds Twitter's 140 character limit.", colors.error)
  609. return
  610. }
  611. println "Update status: '$status'? "
  612. if (stdin.nextLine() ==~ /yes|y|true|t/) {
  613. try {
  614. twitter.updateStatus(status)
  615. println "Status posted."
  616. } catch (Exception e) {
  617. println "An error occurred trying to post the status: '" +
  618. e.localizedMessage
  619. log.error("Error posting status:", e)
  620. }
  621. } else println "Status post canceled."
  622. }
  623. public void retweetStatus(def statusId) {
  624. log.debug("Retweeting a status: '{}'", statusId)
  625. if (!statusId.isLong() || !statusId) {
  626. println color("retweet ", colors.option) +
  627. color("command requires a status id: ", colors.error) +
  628. "gritter post retweet <statusId>"
  629. }
  630. statusId = statusId as long
  631. def status = twitter.showStatus(statusId)
  632. println "Retweet '" + color(status.text, colors.odd) + "'? "
  633. if (stdin.nextLine() ==~ /yes|y|true|t/) {
  634. twitter.retweetStatus(statusId)
  635. println "Status retweeted."
  636. } else { println "Retweet cancelled." }
  637. }
  638. public void addListMember(LinkedList args) {
  639. def listRef = parseListReference(args)
  640. def user = args.poll()
  641. log.debug("Adding a member to a list: list='{}', user='{}'",
  642. listRef, user)
  643. if (!user) {
  644. println color("add list member", colors.option) +
  645. color(" requires two parameters: ", colors.error) +
  646. "gritter add list member <list-ref> <user>"
  647. return
  648. }
  649. if (!listRef) {
  650. println color("No list found that matches the given description: ",
  651. colors.error) + color(listRef, colors.option)
  652. return
  653. }
  654. // look up the user id if neccessary
  655. if (user.isLong()) user = user as long
  656. else user = twitter.showUser(user).id
  657. twitter.addUserListMember(listRef.listId, user)
  658. // TODO: error checking?
  659. println "Added list member."
  660. }
  661. public void addListSubscription(LinkedList args) {
  662. def listRef = parseListReference(args)
  663. log.debug("Subscribing to a list: list='{}'", listRef)
  664. if (!listRef) {
  665. println color("add list subscription ", colors.option) +
  666. color("expects a list name, user/list: ", colors.error) +
  667. "gritter add list subscription <user/list>"
  668. return
  669. }
  670. twitter.subscribeUserList(listRef.username, listRef.listId)
  671. // TODO: error checking?
  672. println "Subscribed to list."
  673. }
  674. public void createNewList(LinkedList args) {
  675. def name = args.poll()
  676. def isPublic = args.poll()
  677. def desc = args.poll()
  678. log.debug("Creating a new list: name='{}', isPublic='{}', desc='{}'",
  679. name, isPublic, desc)
  680. if (desc == null) {
  681. println color("create list ", colors.option) +
  682. color("command requires three arguments: ", colors.error) +
  683. "gritter create list <listName> <isPublic> <listDescription>"
  684. return
  685. }
  686. println "Create list '${color(name, colors.option)}'?"
  687. if (stdin.nextLine() ==~ /yes|y|true|t/) {
  688. twitter.createUserList(name, isPublic ==~ /yes|y|true|t/, desc)
  689. println "List created."
  690. } else { println "List creation cancelled." }
  691. }
  692. /// ## Help and Display Functions ##
  693. /// --------------------------------
  694. public void help(LinkedList args) {
  695. log.debug("Processing a 'help' command.")
  696. def command = args.poll()
  697. if (command != null) {
  698. switch (command.toLowerCase()) {
  699. case ~/delete|destroy|remove/: helpDelete(); break
  700. case ~/get|show/: helpGet(); break // get|show
  701. case ~/help/: helpHelp(); break // help
  702. case ~/post|add|create/: helpPost(); break // post|add|create
  703. case ~/reconfigure/: helpReconfigure(); break // reconfigure
  704. case ~/set/: helpSet(); break // set
  705. case~/list-reference/: helpListReference(); break // list-reference
  706. default: // fallthrough
  707. print color("Not a valid command: ", colors.error)
  708. println color(command, colors.option)
  709. break;
  710. }
  711. } else {
  712. println "Gritter v${VERSION} -- A command-line twitter client."
  713. println "Author: Jonathan Bernard <jdbernard@gmail.com>"
  714. println "Website: https://www.github.com/jdbernard/gritter"
  715. println ""
  716. println "usage: " + color("gritter <command> ...", colors.option)
  717. println ""
  718. println "Valid commands (with interchangable aliases):"
  719. println " delete (destroy, remove)"
  720. println " get (show)"
  721. println " help"
  722. println " post (add, create)"
  723. println " reconfigure"
  724. println " set"
  725. println ""
  726. println "use " + color("gritter help <command>", colors.option) +
  727. "for more information about a \nspecific command."
  728. println ""
  729. }
  730. }
  731. public void helpDelete() {
  732. println color("gritter delete ...", colors.option)
  733. println ""
  734. println "Valid uses:"
  735. println ""
  736. println " Destroy a status (tweet) by id:"
  737. println " gritter destroy <status-id>"
  738. println " gritter destroy status <status-id>"
  739. println ""
  740. println " Destroy a list:"
  741. println " gritter destroy list <list-reference>"
  742. println ""
  743. println " Remove a user from a list (stop following them in that list):"
  744. println " gritter remove list member <list-reference> <username>"
  745. println ""
  746. println " Delete list subscription (stop following the list):"
  747. println " gritter delete list subscription <list-reference>"
  748. println ""
  749. println "For more information about list references, use "
  750. println color("gritter help list-reference", colors.option)
  751. }
  752. public void helpGet() {
  753. println color("gritter get ...", colors.option)
  754. println ""
  755. println "Valid uses:"
  756. println ""
  757. println " View a timeline:"
  758. println " gritter show [timeline] (friends|home|mine|public|user <user-id>)"
  759. println ""
  760. println " View available lists for a user (defaults to current user):"
  761. println " gritter show lists"
  762. println " gritter show lists <user-id>"
  763. println ""
  764. println " View a specific list (see the recent tweets):"
  765. println " gritter show list <list-reference>"
  766. println ""
  767. // TODO
  768. //println " View your current list subscriptions:"
  769. //println " gritter show list subscriptions"
  770. //println ""
  771. println " View user information:"
  772. println " gritter show user <user-id>"
  773. println ""
  774. }
  775. public void helpHelp() {
  776. println color("gritter help <command>", colors.option)
  777. println ""
  778. println "Show usage information about a specific gritter command."
  779. println ""
  780. }
  781. public void helpPost() {
  782. println color("gritter post ...", colors.option)
  783. println ""
  784. println "Valid uses:"
  785. println ""
  786. println " Post a status update (tweet):"
  787. println " gritter post <message>"
  788. println " gritter post status <message>"
  789. println ""
  790. println " Retweet a status:"
  791. println " gritter post retweet <status-id>"
  792. println ""
  793. println " Create a new list:"
  794. println " gritter create list <list-name> <is-public?> <description>"
  795. println ""
  796. println " Add a user to a list:"
  797. println " gritter add list member <list-reference> <user-id>"
  798. println ""
  799. println " Subscribe to a list:"
  800. println " gritter add list subscription <list-reference>"
  801. println ""
  802. }
  803. public void helpReconfigure() {
  804. println color("gritter reconfigure", colors.option)
  805. println ""
  806. println "Causes gritter to release resources, forget variables set"
  807. println "manually and reload it's configuration file."
  808. println ""
  809. }
  810. public void helpSet() {
  811. println color("gritter set <key> <value>", colors.option)
  812. println ""
  813. println "Sets a configuration value manually. Configurable values are:"
  814. println ""
  815. println " terminalWidth <integer> Gritter wraps its output to fit "
  816. println " within this width."
  817. println ""
  818. println " colored <boolean> Should gritter's output be in color?"
  819. println " yes,no,true,false are all valid."
  820. println ""
  821. }
  822. public void helpListReference() {
  823. println " a ${color('<list-reference>', colors.option)} is defined as:"
  824. println " ${color('[<username>/]<list-id>', colors.option)}" +
  825. "where username is optional (defaults"
  826. println " to the current user) and list-id may be the list name or "
  827. println " internal id number."
  828. println ""
  829. }
  830. /// ## Utility Functions
  831. /// --------------------
  832. public def parseListReference(LinkedList args) {
  833. def username = args.poll()
  834. def listId
  835. log.debug("Looking up a list: ref = '{}'", username)
  836. log.debug("remaining args='{}'", args)
  837. if (!username) return false
  838. username = username.split("/")
  839. if (username.length != 2) {
  840. listId = username[0]
  841. username = twitter.screenName
  842. } else {
  843. listId = username[1]
  844. username = username[0]
  845. }
  846. if (listId.isInteger()) listId = listId as int
  847. else listId = findListByName(username, listId)
  848. // TODO: err if list does not exist
  849. return [username: username, listId: listId]
  850. }
  851. public int findListByName(String userName, String listName) {
  852. def userLists
  853. long cursor = -1
  854. int listId = -1
  855. while(listId == -1) {
  856. userLists = twitter.getUserLists(userName, cursor)
  857. userLists.each { list ->
  858. if (listName == list.name || listName == list.slug)
  859. listId = list.id
  860. }
  861. if (!userLists.hasNext()) break
  862. cursor = userLists.nextCursor
  863. }
  864. return listId
  865. }
  866. public String formatStatus(Status status, Map formatOptions) {
  867. def indent = formatOptions.indent ?: ""
  868. def authorLength = formatOptions.authorLength ?: 0
  869. def rowNum = formatOptions.rowNum ?: 0
  870. String result
  871. def textColor = (rowNum % 2 == 0 ? colors.even : colors.odd)
  872. // add author's username
  873. result = color(status.user.screenName.padLeft(
  874. authorLength) + ": ", colors.author, textColor)
  875. // format the status text
  876. String text = status.text
  877. // if this status takes up more room than we have left on the line
  878. if (text.length() > terminalWidth - indent.length()) {
  879. // wrap text to terminal width
  880. text = wrapToWidth(text, terminalWidth, indent, "").
  881. substring(indent.length())
  882. // if we are
  883. }
  884. // color @mentions in the tweet
  885. text = text.replaceAll(/(@\w+)/, color("\$1", colors.mentioned, textColor))
  886. result += text
  887. return result
  888. }
  889. public static void printUsage() {
  890. // TODO
  891. }
  892. public String resetColor() { colored ? "\u001b[m" : "" }
  893. public String color(def message, ConsoleColor color,
  894. ConsoleColor existing = null) {
  895. if (!colored) return message
  896. return color.toString() + message + (existing ?: resetColor())
  897. }
  898. static String wrapToWidth(String text, int width, String prefix, String suffix) {
  899. int lastSpaceIdx = 0;
  900. int curLineLength = 0;
  901. int lineStartIdx = 0;
  902. int i = 0;
  903. int actualWidth = width - prefix.length() - suffix.length()
  904. String wrapped = ""
  905. text = text.replaceAll("[\n\r]", " ")
  906. for (i = 0; i < text.length(); i++) {
  907. curLineLength++
  908. if (curLineLength > actualWidth) {
  909. if (lastSpaceIdx == -1) // we haven't seen a space on this line
  910. lastSpaceIdx = lineStartIdx + actualWidth - 1
  911. wrapped += prefix + text[lineStartIdx..<lastSpaceIdx] + suffix + EOL
  912. curLineLength = 0
  913. lineStartIdx = lastSpaceIdx + 1
  914. i = lastSpaceIdx + 1
  915. lastSpaceIdx = -1
  916. }
  917. if (text.charAt(i).isWhitespace())
  918. lastSpaceIdx = i
  919. }
  920. if (i - lineStartIdx > 1)
  921. wrapped += prefix + text[lineStartIdx..<text.length()]
  922. return wrapped
  923. }
  924. }