Hypermedia APIs - less hype more media, please

Hypermedia APIs - less hype more media, please

Hypermedia-driven APIs put more burden on client developers. Sometimes that's worth it.

7e19cd5486b5d6dc1ef90e671ba52ae0?s=128

Wynn Netherland

March 02, 2013
Tweet

Transcript

  1. [ LESS HYPE, MORE MEDIA, PLEASE ] HYPERMEDIA APIs

  2. WYNNNETHERLAND

  3. @pengwynn

  4. @pengwynn

  5. WYNN.FM

  6. None
  7. None
  8. None
  9. None
  10. None
  11. None
  12. None
  13. I write API wrappers.

  14. None
  15. Wynn Netherland Flightless waterfowl

  16. Wynn Netherland Defacto API Evangelist

  17. None
  18. Our industry loves buzzwords.

  19. “Nice site. Is it RESPONSIVE?”

  20. “Nice database. Is it SCHEMALESS?”

  21. “Nice API. Is it HYPERMEDIA-DRIVEN?”

  22. Nice questions. HACKER NEWS much?

  23. WHAT THE HECK IS HYPERMEDIA?

  24. >

  25. hubot wiki me hypermedia Hypermedia is used as a logical

    extension of the term hypertext in which graphics, audio, video, plain text and hyperlinks intertwine to create a generally non-linear medium of information. This contrasts with the broader term multimedia, which may be used to describe non-interactive linear presentations as well as hypermedia. It is also related to the field of electronic literature. The term was first used in a 1965 article by Ted Nelson. https://en.wikipedia.org/wiki/Hypermedia >
  26. hubot wiki me hypermedia Hypermedia is used as a logical

    extension of the term hypertext in which graphics, audio, video, plain text and hyperlinks intertwine to create a generally non-linear medium of information. This contrasts with the broader term multimedia, which may be used to describe non-interactive linear presentations as well as hypermedia. It is also related to the field of electronic literature. The term was first used in a 1965 article by Ted Nelson. https://en.wikipedia.org/wiki/Hypermedia >
  27. hypermedia Hypermedia is used as a logical extension of the

    term hypertext in which graphics, audio, video, plain text and hyperlinks intertwine to create a generally non-linear medium of information.
  28. hypermedia Hypermedia is used as a logical extension of the

    term hypertext in which graphics, audio, video, plain text and hyperlinks intertwine to create a generally non-linear medium of information.
  29. hypertext Hypertext is text displayed on a computer display or

    other electronic device with references (hyperlinks) to other text that the reader can immediately access, usually by a mouse click, keypress sequence or by touching the screen.
  30. hypertext Hypertext is text displayed on a computer display or

    other electronic device with references (hyperlinks) to other text that the reader can immediately access, usually by a mouse click, keypress sequence or by touching the screen.
  31. hyperlink In computing, a hyperlink (or link) is a reference

    to data that the reader can directly follow, or that is followed automatically. A hyperlink points to a whole document or to a specific element within a document.
  32. hyperlink In computing, a hyperlink (or link) is a reference

    to data that the reader can directly follow, or that is followed automatically. A hyperlink points to a whole document or to a specific element within a document.
  33. None
  34. None
  35. None
  36. HATEOAS

  37. HATEOAS Rhymes with Cheerios

  38. HATEOAS Rhymes with Cheerios

  39. HATEOAS

  40. HATEOAS A REST client enters a REST application through a

    simple fixed URL. *
  41. HATEOAS A REST client enters a REST application through a

    simple fixed URL. * All future actions the client may take are discovered within resource representations returned from the server. *
  42. HATEOAS A REST client enters a REST application through a

    simple fixed URL. * All future actions the client may take are discovered within resource representations returned from the server. * The media types used for these representations, and the link relations they may contain, are standardized. *
  43. A SIMPLE EXAMPLE

  44. $ curl https://status.github.com/api.json

  45. $ curl https://status.github.com/api.json API root, our simple, fixed URL

  46. $ curl https://status.github.com/api.json {"status_url":"https://status.github.com/api/ status.json","messages_url":"https://status.github.com/ api/messages.json","last_message_url":"https:// status.github.com/api/last-message.json"} Resource representation

  47. $ curl https://status.github.com/api.json {"status_url":"https://status.github.com/api/ status.json","messages_url":"https://status.github.com/ api/messages.json","last_message_url":"https:// status.github.com/api/last-message.json"} Link relation

  48. $ curl https://status.github.com/api.json \ | jq -r '.status_url' https://status.github.com/api/status.json

  49. $ curl https://status.github.com/api.json \ | jq -r '.status_url' https://status.github.com/api/status.json

  50. $ curl https://status.github.com/api.json \ | jq -r '.status_url' | xargs

    curl Follow link relation
  51. $ curl https://status.github.com/api.json \ | jq -r '.status_url' | xargs

    curl {"status":"good","last_updated":"2013-02-27T21:31:55Z"}
  52. $ curl https://status.github.com/api.json \ | jq -r '.status_url' | xargs

    curl {"status":"good","last_updated":"2013-02-27T21:31:55Z"}
  53. $ curl https://status.github.com/api.json \ | jq -r '.status_url' | xargs

    curl \ | jq -r '.status'
  54. DETOUR

  55. brew install jq

  56. THE REAL J-QUERY™

  57. curl https://api.github.com/repos/pengwynn/octokit/commits \ | jq -r '.[].author.url' | uniq -u

    https://api.github.com/users/patcon https://api.github.com/users/pengwynn https://api.github.com/users/sferik https://api.github.com/users/joeyw https://api.github.com/users/camelmasa https://api.github.com/users/pengwynn
  58. curl https://api.github.com/repos/pengwynn/octokit/commits \ | jq -r '.[].author.url' | uniq -u

    https://api.github.com/users/patcon https://api.github.com/users/pengwynn https://api.github.com/users/sferik https://api.github.com/users/joeyw https://api.github.com/users/camelmasa https://api.github.com/users/pengwynn
  59. curl https://api.github.com/repos/pengwynn/octokit/commits \ | jq -r '.[].author.url' | uniq -u

    https://api.github.com/users/patcon https://api.github.com/users/pengwynn https://api.github.com/users/sferik https://api.github.com/users/joeyw https://api.github.com/users/camelmasa https://api.github.com/users/pengwynn
  60. HYPERMEDIA TYPES

  61. HATEOAS A REST client enters a REST application through a

    simple fixed URL. * All future actions the client may take are discovered within resource representations returned from the server. * The media types used for these representations, and the link relations they may contain, are standardized. *
  62. “Hypermedia Types are MIME media types that contain native hyper-linking

    semantics that induce application flow. For example, HTML is a hypermedia type; XML is not.” Mike Amundsen
  63. “Hypermedia Types are MIME media types that contain native hyper-linking

    semantics that induce application flow. For example, HTML is a hypermedia type; XML is not.” Mike Amundsen
  64. Neither is JSON.

  65. BUT <HTML/> SUCKS FOR {DATA}.

  66. BUT <HTML/> SUCKS FOR {DATA}. remember Microformats?

  67. “Hypermedia Types are MIME media types...”

  68. “Hypermedia Types are MIME media types...” “...because a MIME is

    a terrible thing to waste.” amirite?
  69. HAL

  70. None
  71. HAL provides simple linking in data { "_links": { "self":

    { "href": "/orders" }, "next": { "href": "/orders?page=2" }, "find": { "href": "/orders{?id}", "templated": true }, "admin": [ { "href": "/admins/2", "title": "Fred" }, { "href": "/admins/5", "title": "Kate" } ] }, currentlyProcessing: 14, shippedToday: 20, "_embedded": { "orders": [{ "_links": { "self": { "href": "/orders/123" }, ... application/hal+json
  72. HAL provides simple linking in data { "_links": { "self":

    { "href": "/orders" }, "next": { "href": "/orders?page=2" }, "find": { "href": "/orders{?id}", "templated": true }, "admin": [ { "href": "/admins/2", "title": "Fred" }, { "href": "/admins/5", "title": "Kate" } ] }, currentlyProcessing: 14, shippedToday: 20, "_embedded": { "orders": [{ "_links": { "self": { "href": "/orders/123" }, ... application/hal+json in two flavors.*
  73. HAL provides simple linking in data { "_links": { "self":

    { "href": "/orders" }, "next": { "href": "/orders?page=2" }, "find": { "href": "/orders{?id}", "templated": true }, "admin": [ { "href": "/admins/2", "title": "Fred" }, { "href": "/admins/5", "title": "Kate" } ] }, currentlyProcessing: 14, shippedToday: 20, "_embedded": { "orders": [{ "_links": { "self": { "href": "/orders/123" }, ... application/hal+json in two flavors.* * but one is XML and has 52% more fiber.
  74. HAL provides simple linking in data { "_links": { "self":

    { "href": "/orders" }, "next": { "href": "/orders?page=2" }, "find": { "href": "/orders{?id}", "templated": true }, "admin": [ { "href": "/admins/2", "title": "Fred" }, { "href": "/admins/5", "title": "Kate" } ] }, currentlyProcessing: 14, shippedToday: 20, "_embedded": { "orders": [{ "_links": { "self": { "href": "/orders/123" }, ... application/hal+json
  75. COLLECTION+JSON

  76. COLLECTION+JSON is a JSON-based read/write hypermedia-type designed to support management

    and querying of simple collections.
  77. // sample collection object{ "collection" : { "version" : "1.0",

    "href" : URI, "links" : [ARRAY], "items" : [ARRAY], "queries" : [ARRAY], "template" : {OBJECT}, "error" : {OBJECT} } } COLLECTION+JSON is a JSON-based read/write hypermedia-type designed to support management and querying of simple collections.
  78. // sample collection object{ "collection" : { "version" : "1.0",

    "href" : URI, "links" : [ARRAY], "items" : [ARRAY], "queries" : [ARRAY], "template" : {OBJECT}, "error" : {OBJECT} } } application/vnd.collection+json COLLECTION+JSON is a JSON-based read/write hypermedia-type designed to support management and querying of simple collections.
  79. // sample collection object{ "collection" : { "version" : "1.0",

    "href" : URI, "links" : [ARRAY], "items" : [ARRAY], "queries" : [ARRAY], "template" : {OBJECT}, "error" : {OBJECT} } } application/vnd.collection+json COLLECTION+JSON is a JSON-based read/write hypermedia-type designed to support management and querying of simple collections. Read up on the format in the draft spec.
  80. Why I'm down on Hypermedia Containers ...one of my main

    principles in adopting hypermedia is to avoid educating developers on hypermedia as much as possible. I’m in the game of providing a useful API, not a system that shows off the possibilities of hypermedia and how deeply committed I am to its theories. ADAM KEYS @therealadam
  81. DETOUR

  82. “I think I'll just `curl` up with a nice red

    and read some specs.” - Nobody Evar
  83. What the SPEC? Hammer, et al. Expires November 2, 2012

    [Page 10] Internet-Draft OAuth 2.0 May 2012 +--------+ +---------------+ | |--(A)------- Authorization Grant --------->| | | | | | | |<-(B)----------- Access Token -------------| | | | & Refresh Token | | | | | | | | +----------+ | | | |--(C)---- Access Token ---->| | | | | | | | | | | |<-(D)- Protected Resource --| Resource | | Authorization | | Client | | Server | | Server | | |--(E)---- Access Token ---->| | | | | | | | | | | |<-(F)- Invalid Token Error -| | | | | | +----------+ | | | | | | | |--(G)----------- Refresh Token ----------->| | | | | | | |<-(H)----------- Access Token -------------| | +--------+ & Optional Refresh Token +---------------+ Figure 2: Refreshing an Expired Access Token
  84. What the SPEC? Hammer, et al. Expires November 2, 2012

    [Page 10] Internet-Draft OAuth 2.0 May 2012 +--------+ +---------------+ | |--(A)------- Authorization Grant --------->| | | | | | | |<-(B)----------- Access Token -------------| | | | & Refresh Token | | | | | | | | +----------+ | | | |--(C)---- Access Token ---->| | | | | | | | | | | |<-(D)- Protected Resource --| Resource | | Authorization | | Client | | Server | | Server | | |--(E)---- Access Token ---->| | | | | | | | | | | |<-(F)- Invalid Token Error -| | | | | | +----------+ | | | | | | | |--(G)----------- Refresh Token ----------->| | | | | | | |<-(H)----------- Access Token -------------| | +--------+ & Optional Refresh Token +---------------+ Figure 2: Refreshing an Expired Access Token Page breaks. Online. In 2013.
  85. Specs are a pain to write.

  86. Specs are a pain to read.

  87. If you've ever used an OAuth library, hug the developer.

  88. URI TEMPLATES

  89. curl https://api.github.com/ { current_user_url: "/user", authorizations_url: "/authorizations", emails_url: "/user/emails", emojis_url:

    "/emojis", events_url: "/events", following_url: "/user/following{/target}", gists_url: "/gists{/gist_id}", hub_url: "/hub", issue_search_url: "/legacy/issues/search/{owner}/{repo}/{state}/{keyword}", issues_url: "/issues", keys_url: "/user/keys", notifications_url: "/notifications", organization_repositories_url: "/orgs/{org}/repos/{?type,page,per_page,sort}", organization_url: "/orgs/{org}", public_gists_url: "/gists/public", rate_limit_url: "/rate_limit", repository_url: "/repos/{owner}/{repo}", repository_search_url: "/legacy/repos/search/{keyword}{?language,start_page}", current_user_repositories_url: "/user/repos{?type,page,per_page,sort}", ...
  90. curl https://api.github.com/ { current_user_url: "/user", authorizations_url: "/authorizations", emails_url: "/user/emails", emojis_url:

    "/emojis", events_url: "/events", following_url: "/user/following{/target}", gists_url: "/gists{/gist_id}", hub_url: "/hub", issue_search_url: "/legacy/issues/search/{owner}/{repo}/{state}/{keyword}", issues_url: "/issues", keys_url: "/user/keys", notifications_url: "/notifications", organization_repositories_url: "/orgs/{org}/repos/{?type,page,per_page,sort}", organization_url: "/orgs/{org}", public_gists_url: "/gists/public", rate_limit_url: "/rate_limit", repository_url: "/repos/{owner}/{repo}", repository_search_url: "/legacy/repos/search/{keyword}{?language,start_page}", current_user_repositories_url: "/user/repos{?type,page,per_page,sort}", ...
  91. # create the template tpl = URITemplate.new('https://api.github.com/repos/{owner}/{repo}) # Expand with

    given placeholder values tpl.expand :owner => "pengwynn", :repo => "octokit" => "https://api.github.com/repos/pengwynn/octokit"
  92. repository_search_url: "/legacy/repos/search/{keyword}{?language,start_page}"

  93. HYPERMEDIA AGENTS

  94. SAWYER

  95. SAWYER

  96. endpoint = "http://localhost:9393/" agent = Sawyer::Agent.new(endpoint) root = agent.start root.data.rels[:users].get.data

  97. module Octokit class Halogen LINK_REGEX = /_?url$/ def parse(data) links

    = {} inline_links = data.keys.select {|k| k.to_s[LINK_REGEX] } inline_links.each do |key| rel_name = key.to_s == 'url' ? 'self' : key.to_s.gsub(LINK_REGEX, '') links[rel_name.to_sym] = data[key] end return data, links end end end
  98. FARADAY

  99. None
  100. GARTNER HYPE CYCLE

  101. Technology Trigger GARTNER HYPE CYCLE

  102. Technology Trigger Peak of Inflated Expectations GARTNER HYPE CYCLE

  103. Technology Trigger Peak of Inflated Expectations Trough of Disillusionment GARTNER

    HYPE CYCLE
  104. Technology Trigger Peak of Inflated Expectations Trough of Disillusionment Slope

    of Enlightenment GARTNER HYPE CYCLE
  105. Technology Trigger Peak of Inflated Expectations Trough of Disillusionment Slope

    of Enlightenment Plateau of Productivity GARTNER HYPE CYCLE
  106. API #REALTALK

  107. YOUR API IS HYPOMEDIA

  108. DEMO

  109. DEVELOPERS DON'T READ YOUR DOCS

  110. YOU'RE NOT DOGFOODING IT

  111. BUILD SOMETHING MEANINGFUL WITH YOUR API.

  112. Janky BUILD SOMETHING MEANINGFUL WITH YOUR API.

  113. Janky Heaven BUILD SOMETHING MEANINGFUL WITH YOUR API.

  114. Janky Heaven Monitors BUILD SOMETHING MEANINGFUL WITH YOUR API.

  115. Janky Team Heaven Monitors BUILD SOMETHING MEANINGFUL WITH YOUR API.

  116. Janky Team Hire Heaven Monitors BUILD SOMETHING MEANINGFUL WITH YOUR

    API.
  117. Janky Team Hire Heaven Monitors The Setup™ BUILD SOMETHING MEANINGFUL

    WITH YOUR API.
  118. Janky Team Hire Heaven Monitors The Setup™ Graph Store BUILD

    SOMETHING MEANINGFUL WITH YOUR API.
  119. How GitHub uses the GitHub API.

  120. AuthN How GitHub uses the GitHub API.

  121. AuthN AuthZ How GitHub uses the GitHub API.

  122. AuthN AuthZ Merging How GitHub uses the GitHub API.

  123. AuthN AuthZ Merging Commit Status How GitHub uses the GitHub

    API.
  124. AuthN AuthZ Merging Commit Status GFM How GitHub uses the

    GitHub API.
  125. Do you GET me?

  126. GET /me? HTTP/1.1 200 OK Server: example.com Content-Type: application/json; charset=utf-8

    Connection: keep-alive Status: 200 OK
  127. GET /me? HTTP/1.1 200 OK Server: example.com Content-Type: application/json; charset=utf-8

    Connection: keep-alive Status: 200 OK Developer hears: :OK
  128. GET /me? HTTP/1.1 200 OK Server: example.com Content-Type: application/json; charset=utf-8

    Connection: keep-alive Status: 200 OK Developer hears: :OK
  129. GET /me? HTTP/1.1 500 INTERNAL SERVER ERROR Server: example.com Content-Type:

    application/json; charset=utf-8 Connection: keep-alive Status: 500 INTERNAL SERVER ERROR
  130. GET /me? HTTP/1.1 500 INTERNAL SERVER ERROR Server: example.com Content-Type:

    application/json; charset=utf-8 Connection: keep-alive Status: 500 INTERNAL SERVER ERROR Developer hears: :DOH
  131. GET /me? HTTP/1.1 500 INTERNAL SERVER ERROR Server: example.com Content-Type:

    application/json; charset=utf-8 Connection: keep-alive Status: 500 INTERNAL SERVER ERROR Developer hears: :DOH
  132. GET /me? HTTP/1.1 403 FORBIDDEN Server: example.com Content-Type: application/json; charset=utf-8

    Connection: keep-alive Status: 403 FORBIDDEN
  133. GET /me? HTTP/1.1 403 FORBIDDEN Server: example.com Content-Type: application/json; charset=utf-8

    Connection: keep-alive Status: 403 FORBIDDEN Developer hears: :NOPE
  134. GET /me? HTTP/1.1 302 FOUND Server: example.com Content-Type: application/json; charset=utf-8

    Connection: keep-alive Status: 302 FOUND Location: https://example.com/over/there
  135. GET /me? HTTP/1.1 302 FOUND Server: example.com Content-Type: application/json; charset=utf-8

    Connection: keep-alive Status: 302 FOUND Location: https://example.com/over/there Developer hears: :WAT
  136. /302 me

  137. /302 me The requested resource resides temporarily under a different

    URI. Since the redirection might be altered on occasion, the client SHOULD continue to use the Request-URI for future requests. This response is only cacheable if indicated by a Cache- Control or Expires header field.
  138. ETAGS ARE COOL. NOBODY USES 'EM.

  139. curl -I https://api.github.com/users/defunkt HTTP/1.1 200 OK Server: nginx Date: Wed,

    12 Sep 2012 14:07:43 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Status: 200 OK Content-Length: 692 X-Content-Type-Options: nosniff X-RateLimit-Remaining: 4997 X-RateLimit-Limit: 5000 Cache-Control: public, s-maxage=60, max-age=60 Vary: Accept X-GitHub-Media-Type: github.beta ETag: "ef742caec0c19e2169ffb05e7d200d17" Last-Modified: Tue, 11 Sep 2012 02:52:21 GMT
  140. curl -I https://api.github.com/users/defunkt HTTP/1.1 200 OK Server: nginx Date: Wed,

    12 Sep 2012 14:07:43 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Status: 200 OK Content-Length: 692 X-Content-Type-Options: nosniff X-RateLimit-Remaining: 4997 X-RateLimit-Limit: 5000 Cache-Control: public, s-maxage=60, max-age=60 Vary: Accept X-GitHub-Media-Type: github.beta ETag: "ef742caec0c19e2169ffb05e7d200d17" Last-Modified: Tue, 11 Sep 2012 02:52:21 GMT
  141. curl -I https://api.github.com/users/defunkt HTTP/1.1 200 OK Server: nginx Date: Wed,

    12 Sep 2012 14:07:43 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Status: 200 OK Content-Length: 692 X-Content-Type-Options: nosniff X-RateLimit-Remaining: 4997 X-RateLimit-Limit: 5000 Cache-Control: public, s-maxage=60, max-age=60 Vary: Accept X-GitHub-Media-Type: github.beta ETag: "ef742caec0c19e2169ffb05e7d200d17" Last-Modified: Tue, 11 Sep 2012 02:52:21 GMT Cache policy
  142. curl -I https://api.github.com/users/defunkt HTTP/1.1 200 OK Server: nginx Date: Wed,

    12 Sep 2012 14:07:43 GMT Content-Type: application/json; charset=utf-8 Connection: keep-alive Status: 200 OK Content-Length: 692 X-Content-Type-Options: nosniff X-RateLimit-Remaining: 4997 X-RateLimit-Limit: 5000 Cache-Control: public, s-maxage=60, max-age=60 Vary: Accept X-GitHub-Media-Type: github.beta ETag: "ef742caec0c19e2169ffb05e7d200d17" Last-Modified: Tue, 11 Sep 2012 02:52:21 GMT Fingerprint
  143. curl -I \ -H 'If-None-Match:"ef742caec0c19e2169ffb05e7d200d17" \ https://api.github.com/users/defunkt HTTP/1.1 304 Not

    Modified Server: nginx Date: Wed, 12 Sep 2012 15:51:39 GMT Connection: keep-alive Status: 304 Not Modified X-RateLimit-Limit: 5000 X-Content-Type-Options: nosniff Vary: Accept ETag: "ef742caec0c19e2169ffb05e7d200d17" X-RateLimit-Remaining: 4997 Last-Modified: Wed, 12 Sep 2012 01:38:14 GMT Cache-Control: public, s-maxage=60, max-age=60
  144. curl -H 'If-None-Match:"ef742caec0c19e2169ffb05e7d200d17" \ https://api.github.com/users/defunkt HTTP/1.1 304 Not Modified Server:

    nginx Date: Wed, 12 Sep 2012 15:51:39 GMT Connection: keep-alive Status: 304 Not Modified X-RateLimit-Limit: 5000 X-Content-Type-Options: nosniff Vary: Accept ETag: "ef742caec0c19e2169ffb05e7d200d17" X-RateLimit-Remaining: 4997 Last-Modified: Wed, 12 Sep 2012 01:38:14 GMT Cache-Control: public, s-maxage=60, max-age=60
  145. $ curl -i https://api.github.com/user HTTP/1.1 200 OK Cache-Control: private, max-age=60

    ETag: "644b5b0155e6404a9cc4bd9d8b1ae730" Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT Status: 200 OK Vary: Accept, Authorization, Cookie X-RateLimit-Limit: 5000 X-RateLimit-Remaining: 4996 $ curl -i https://api.github.com/user -H "If-Modified-Since: Thu, 05 Jul 2012 15:31:30 GMT" HTTP/1.1 304 Not Modified Cache-Control: private, max-age=60 Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT Status: 304 Not Modified Vary: Accept, Authorization, Cookie X-RateLimit-Limit: 5000 X-RateLimit-Remaining: 4996 $ curl -i https://api.github.com/user -H 'If-None-Match: "644b5b0155e6404a9cc4bd9d8b1ae730"' HTTP/1.1 304 Not Modified Cache-Control: private, max-age=60 ETag: "644b5b0155e6404a9cc4bd9d8b1ae730" Last-Modified: Thu, 05 Jul 2012 15:31:30 GMT Status: 304 Not Modified
  146. N+1 OVER HTTP IS EXPENSIVE, YO

  147. Attribution Hand designed by Naomi Atkinson from The Noun Project

    Cereal designed by Jacob Halton from The Noun Project Evil designed by Jim Lears from The Noun Project Console designed by Austin Andrews from The Noun Project Report designed by Doug Cavendish from The Noun Project Television designed by Piero Borgo from The Noun Project Person designed by Paulo Sá Ferreira from The Noun Project Detour designed by Dmitry Baranovskiy from The Noun Project Mime designed by Jonathan C. Dietrich from The Noun Project
  148. Thanks.