Browse Source

initial release

Davide Alberani 5 years ago
commit
00c411d33f
11 changed files with 762 additions and 0 deletions
  1. 201 0
      LICENSE.txt
  2. 27 0
      README.md
  3. 27 0
      ssl/sb-cert.key
  4. 19 0
      ssl/sb-cert.pem
  5. 27 0
      ssl/sb-root-ca.key
  6. 21 0
      ssl/sb-root-ca.pem
  7. 1 0
      ssl/sb-root-ca.srl
  8. 19 0
      static/css/sb.css
  9. 25 0
      static/index.html
  10. 160 0
      static/js/sb.js
  11. 235 0
      toot-my-t-shirt

+ 201 - 0
LICENSE.txt

@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.

+ 27 - 0
README.md

@@ -0,0 +1,27 @@
+# toot-my-t-shirt
+
+A work-in-progress project for 35C3 (from an idea by itec)
+
+Still not much to see here: the UI still sucks and very little feedback is given.
+
+
+# Run
+
+```
+./toot-my-t-shirt --debug --mastodon-token=C0FEFE --mastodon-api-url=https://botsin.space/ --default-image-description="a nice description of the picture" --default-message="oh noes, another selfie" --store-dir="/tmp/selfies"
+```
+
+# License and copyright
+
+Copyright 2018 Davide Alberani <da@mimante.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+

+ 27 - 0
ssl/sb-cert.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEAtzZ9xNUK3GiGeQb3HQxjOVwevXa6KSppIJJwyJRkXEI4D6Bb
+9A9Ayd2uaBke/SRPBUupi0I2BLoYDZetLrdyXYUgXcTq2af2vgtlx9ZFq8J2ntMk
+o6CwvyXvLUZTbUQkkcrCHOP2geixpMF0WOjv1PkqfD03SOq0vs85zU/NrLlRn2gU
+6FOwDMsiMnjEiknCVW+298hDlOOdd6ldia4k+ogZ2jUt91qErVaaq1qdnEGKDO1o
+N5ccRlUZU8YLQvjDKVW4W8ttQ7mKp+0DeHLKySuVvhDqzeItPEErlOJlzcoeon5f
+C5s6wmKSE3JZumQvsOFIVqiaE9GKAmB+ndNI7QIDAQABAoIBAQCdj44/vVu2y2mC
+IdxYrfOTO8bv93AHwQJh0a6OwRdCRGyD+8u4q3lzYWMBAUGmQBh5HGW1bn6YOBZB
+ckSsnXUMOlXoblXuU0WekJy6bGrEWNu8oSasVaBK8uurSwSqPmUYwH+Jav7vH9fO
+MdTGNaUzygigieDGo5pHUl2KVOwzckKplA8IXjknMlJjC9y6eEn4bVtUMhSn2EqL
+pqZvjfkbQ+zk4oC1qOatSdlse3vTut3oV0G9cCpVL1l7a2qF7SyFTGji+GxMT5df
+IcvjgTqB6h/dTiulo9QS5GXBK+CYysnPPccbFx26io5n9NQxIPHUxnBQEODGcRDy
+SPvgjvw9AoGBAOrI/KrNqFTjLZlzWxiBeDbM/DAwqiF2F1uORJ+lVhp2G9GgkU6J
+K84GP335J0Yul8Tl2xajBC+0iwpxLvlpBIHWAilrCweDRqEvpxIHPEQZ9q07hM45
+O+8F0piSJxv8tpkU1oem/4Yl5ZRkjKu9RPZTQTE5aUsa53pK6ALxbAv/AoGBAMfE
+iv9vNzqrsPVSBFUXeK9c3i1eHps5MCDhpx7NY/0MeUpFHcgE+XbgIyQZKZHoVZeW
+4oacgBo8EkHROK37S3wvXv6hUAFd8HQwMpkYmYWs5qOSYrghxZJOpgWY7toTqrx0
+QynAd7W2SCSvLX/ZjUH/QH3GCYVmQOx1fLc0dZsTAoGAYGEnT5pi6o3jjyWKlLG5
+Po3BTKr9fAT1K7FoPDzr7qrjWpdWbu3iXI22DKl11NqVlM9is5Uxx7+OgDfcN6hD
+oGTQuF3nxiq+mLZuF/l+ZNpfp9dR+jIGh2VVgSomAdgowQiL1F3acSAncVYhZPKq
+V4/vqBxQO/OMaGhNe7/NQdMCgYBCoUCHUC4IqKl+OZvuUcTUINKOKT1mIp316a3X
+LURza4ytA/6Z72bRipLOAIKIAwlBZXcq1No5Zd3lDAauqQmVYyt5HI7V1eJUrprB
+y52xI2lOF45Lwh/m28quRUMtg6/H6bNZIrQK7MCFU9SGNybRY3S8PqiAUQnIlKtD
+ZADx9wKBgQCDWYTVMevIOmPRkvAWpS5pRNMl0xqdWZXb9VIub0drB3Qf8VVuc6NA
+sMGHV/pLxiB9FpZiNwH66TEOuZfbPbiRnqOvgc84zEJHHt5mpVVMCwAaF0RQ2v3g
+p7EBHTEvtFchvsJr77PcOUOjgy5tuMlJPL6q8kX+1eAcMOUR6RNkJg==
+-----END RSA PRIVATE KEY-----

+ 19 - 0
ssl/sb-cert.pem

@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDFTCCAf0CCQC/cZgBEqLmgzANBgkqhkiG9w0BAQsFADBIMQswCQYDVQQGEwJV
+UzEZMBcGA1UECAwQQSBWZXJ5IEJhZCBTdGF0ZTELMAkGA1UECgwCU0IxETAPBgNV
+BAMMCHNiLmxvY2FsMB4XDTE4MTEyNTE3MDQyNFoXDTIxMDkxNDE3MDQyNFowUTEL
+MAkGA1UEBhMCVVMxGTAXBgNVBAgMEEEgVGVycmlibGUgU3RhdGUxFDASBgNVBAoM
+C1NlbGZpZSBCb290MREwDwYDVQQDDAhzYi5sb2NhbDCCASIwDQYJKoZIhvcNAQEB
+BQADggEPADCCAQoCggEBALc2fcTVCtxohnkG9x0MYzlcHr12uikqaSCScMiUZFxC
+OA+gW/QPQMndrmgZHv0kTwVLqYtCNgS6GA2XrS63cl2FIF3E6tmn9r4LZcfWRavC
+dp7TJKOgsL8l7y1GU21EJJHKwhzj9oHosaTBdFjo79T5Knw9N0jqtL7POc1Pzay5
+UZ9oFOhTsAzLIjJ4xIpJwlVvtvfIQ5TjnXepXYmuJPqIGdo1LfdahK1WmqtanZxB
+igztaDeXHEZVGVPGC0L4wylVuFvLbUO5iqftA3hyyskrlb4Q6s3iLTxBK5TiZc3K
+HqJ+XwubOsJikhNyWbpkL7DhSFaomhPRigJgfp3TSO0CAwEAATANBgkqhkiG9w0B
+AQsFAAOCAQEAtBMvTBSdohObBFtanrfR7xEfw8JANB1OoUXEsspmiziOpM428x8v
+jBufS6Z+VyEa2mEXsuFnzkMXHi4DU6Y36dlVDWP3qK4u+vvpa8/iKsDyjNNQoulX
+gh9GLl6ed2TYeLmhetYU5wYIKGvvzq2oUBp/VPJNDDdffX58MxykG2OZUuRn5EIJ
+muY8znDmUpDGVXYPjy9Nh7UypIhruKGdtQfWBPaSkJsrNS/E05/i5I+s8M/UXzGA
+EGI65J2GDXD5cVX0n0BoMrC2bra/Vsf/CGlaUiDLsvBK4gl1GIY6V49/dVS2F5QK
+ua9RSeBwxEINDLvqjwlc/EromHqFTbLqTg==
+-----END CERTIFICATE-----

+ 27 - 0
ssl/sb-root-ca.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAxt1k/FELm3mLD8F5otcOcdSHYSiFDUhIvz4Nc3YoWhCIjRRd
+xfzwXfxGGzQaZpmbLZZVPtXNTS83Yd7NzVr8WIGYPMcTnAajT2mpNtsDHcFkxs3D
+M0aK+rb9S8r7hbvPlPraLGbznP2ImFRlMPLE4KeEPbs5FmxCwRiGVVyVoFZgu01u
+hyWayAG3CK/YelTvuZN/lZdnVSe3a4vR9N33T9gU304qqbRT6GZ3yawd4gutltp8
+ZFHadZbUmYAQK2sugZMw8f8kEynuDKmztPiBCA+TaYQDuUCWckTykOWHFciGR+PQ
+A+/GHrB9XfDscX3lSBEicme3GJ1VaAqgeCxvyQIDAQABAoIBAFPfoq0MnaGoZK9z
+gZLds1jtM2AWD+/nMc9/I3s0NZau7HjcQySzJsntEcB9fDkTxjA2/KMw15MbO/eK
+WjCnlFDb79KKgEnJPu3KebUKMElHfPKgbBjfQtS1gyWJagYgjU+fcY9SqKLpB8h/
+p+I6MjEyVgMXSN+dL5ZzeozcLLtflDzmM0SyLNKRKWGPD8mFweH7FSYhsjc2Oqua
+oy859clz62ecDmR8eSDWZ7g3oy2HzaypD/VW7rRiksR02wGaFvhLnf5DbqJ31ek+
+Ln0Gdi+8eTEpqBkddphxkx23AsIb8OPtvbeRE05awhzZF0I1kyFPahcq+rEkHBHY
+F/ACpYECgYEA8rh1B7dYLskERbsVeWH6iwylztQ6ouGCChNUTRmQOSxL1SnSF/nV
+8wAmPyQYj4dtq/t+/VL80VlAehj/wQDBKBj21Xwjom+NEhUn2bhfD9hCQeAkL5bw
+2ienHxwbRR/ouuNnHxfUHKj8Up9OA/rHMZGif3vq5vlm+pOoPmvgtXkCgYEA0b6x
+wrmPq4FY5GPyeVGOloFfGPvCGsoNx1WOUhhWGLkIaUeEcgoYsXPYmOf4MsFRvoxu
+HZ9lnQsZUXGfDbEwJXMw/Bz/Z8NbbofujFXkFrVSCbdcDofSnSoBPCDQXa3nsdTq
+xx6YUKfRVjHLExxSbGiaFDxy9b3PU6b93mvQiNECgYARgbp3NwM2RKt5OBhBbA69
+Lsla1LXx/5/4iBJhiUF8zjQeCOktb4i+ATnA/iKDX7pKWFZ9gRnZI73h0KHJ0vsb
+oElVdqG/WpprPnlkW8cHhoqo47jYceOnaIrGVKmm37lSmYpblMVo18tzTig7Y0Aw
+1BdLaK21wTFrS3EsJ23KyQKBgQCLwIbK2z8aJEYpb1r5cNkT+UF28RCFLwn9Pklk
+8+gx8t/i3g8muQl4+1pfj3h1wQ+JaiJYxIM9H08QUCeNRPlyio0h/uRCrA042YOd
+qAEhDFGMPcstt1wi8gD+olKTiLMvb1G7uOv+GcNGrkjEBAP7TbsULq7ehEknUMYo
+tCevcQKBgA4nuBrgrAUWKu86akaIAT20igTUJcYOFsJR44ElB8PLPtE6ogOacKCk
+g018Kf5oWPt3FlCWqk4/ey/rNopl4OWve933CSJ243XaKfMViMP4Hrj1nzZCAyr+
+/RwjRhZ/g3OrMJlXQ12q/aPn+60vWimwFWS6tzQFEZhVfkGWKZeh
+-----END RSA PRIVATE KEY-----

+ 21 - 0
ssl/sb-root-ca.pem

@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDZjCCAk6gAwIBAgIJAP6Gpxjuai73MA0GCSqGSIb3DQEBCwUAMEgxCzAJBgNV
+BAYTAlVTMRkwFwYDVQQIDBBBIFZlcnkgQmFkIFN0YXRlMQswCQYDVQQKDAJTQjER
+MA8GA1UEAwwIc2IubG9jYWwwHhcNMTgxMTI1MTcwMTQxWhcNMjEwOTE0MTcwMTQx
+WjBIMQswCQYDVQQGEwJVUzEZMBcGA1UECAwQQSBWZXJ5IEJhZCBTdGF0ZTELMAkG
+A1UECgwCU0IxETAPBgNVBAMMCHNiLmxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAxt1k/FELm3mLD8F5otcOcdSHYSiFDUhIvz4Nc3YoWhCIjRRd
+xfzwXfxGGzQaZpmbLZZVPtXNTS83Yd7NzVr8WIGYPMcTnAajT2mpNtsDHcFkxs3D
+M0aK+rb9S8r7hbvPlPraLGbznP2ImFRlMPLE4KeEPbs5FmxCwRiGVVyVoFZgu01u
+hyWayAG3CK/YelTvuZN/lZdnVSe3a4vR9N33T9gU304qqbRT6GZ3yawd4gutltp8
+ZFHadZbUmYAQK2sugZMw8f8kEynuDKmztPiBCA+TaYQDuUCWckTykOWHFciGR+PQ
+A+/GHrB9XfDscX3lSBEicme3GJ1VaAqgeCxvyQIDAQABo1MwUTAdBgNVHQ4EFgQU
+QjvpZHiVO/I9WIMZjmBY2cgpuQowHwYDVR0jBBgwFoAUQjvpZHiVO/I9WIMZjmBY
+2cgpuQowDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAad40y53p
+eiCb/EPOXGMpXRpFy4tt0HhZlbmrdpVpg1Aykc2gkf9xPYJna5Q1oWd6zCcY37Sr
+yOOW3sPNyUTQGTxww+5zLXlFnKJs2w7rxUrnbMKqsWyHAsSQDj3g9uAxkxsDG14K
+UKwUrSg8AcezhnSnJUOM3rnAFWK78osDH32hiizqJ0B3HS4fqZ0/Tn6pb4bIMu6l
+3IJIGObLb3jFw7LBGtxFhAxyB8y8WPwTvirpXyib4uqHEjVgMm5EpC6ZbS0JoIwR
+GgJwgUn+cESMuv8kwq3b3M20GfisY8OgqH9xrCj+q/3MeIOEro/xiAB3mMzLlKiN
+Cyolvxa5IuqNOw==
+-----END CERTIFICATE-----

+ 1 - 0
ssl/sb-root-ca.srl

@@ -0,0 +1 @@
+BF71980112A2E683

+ 19 - 0
static/css/sb.css

@@ -0,0 +1,19 @@
+
+#sb-video {
+    width: 320px;
+    height: 240px;
+    margin: 15px;
+    float: left;
+}
+
+#sb-canvas {
+    width: 320px;
+    height: 240px;
+    margin: 15px;
+    float: left;
+}
+
+button {
+    clear:both;
+    margin: 30px;
+}

+ 25 - 0
static/index.html

@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <meta charset="utf-8">
+        <title>toot-my-t-shirt</title>
+        <link rel="icon" type="image/png" href="/static/images/sb-favicon.png">
+        <link rel="stylesheet" type="text/css" href="/static/css/sb.css">
+        <script type="text/javascript" src="/static/js/sb.js"></script>
+    </head>
+    <body>
+        <div id="main">
+            <h1>toot-my-t-shirt</h1>
+            <div id="video-container">
+                <video id="sb-video" autoplay="true" muted="muted"></video>
+            </div>
+            <div id="canvas-container">
+                <canvas id="sb-canvas"></canvas>
+            </div>
+            <button id="init-btn" name="init-btn" onClick="initCamera()">Run!</button>
+            <button id=take-photo-btn" name="take-photo-btn" onClick="takePhoto()">Take photo!</button>
+            <button id=send-photo-btn" name="send-photo-btn" onClick="sendPhoto()">Share photo!</button>
+            <button id=cancel-photo-btn" name="cancel-photo-btn" onClick="cancelPhoto()">Cancel photo</button>
+        </div>
+    </body>
+</html>

+ 160 - 0
static/js/sb.js

@@ -0,0 +1,160 @@
+var countdown = {
+    _timeout: null,
+    _stepCb: null,
+    _timeoutCb: null,
+    running: false,
+    seconds: 5,
+    _initial_seconds: 5,
+
+    start: function(seconds, timeoutCb, stepCb) {
+        countdown.stop();
+        countdown.seconds = countdown._initial_seconds = seconds || 5;
+        countdown._timeoutCb = timeoutCb || countdown._timeoutCb;
+        countdown._stepCb = stepCb || countdown._stepCb;
+        countdown.running = true;
+        countdown._step();
+    },
+
+    stop: function() {
+        if (countdown._timeout) {
+            window.clearTimeout(countdown._timeout);
+        }
+        countdown.running = false;
+    },
+
+    restart: function() {
+        countdown.start(countdown._initial_seconds);
+    },
+
+    _step: function() {
+        if (countdown._stepCb) {
+            countdown._stepCb();
+        }
+        if (countdown.seconds === 0) {
+            if (countdown._timeoutCb) {
+                countdown._timeoutCb();
+            }
+            countdown.stop();
+        } else {
+            countdown._decrement();
+        }
+    },
+
+    _decrement: function() {
+        countdown.seconds = countdown.seconds - 1;
+        countdown._timeout = window.setTimeout(function() {
+            countdown._step();
+        }, 1000);
+    }
+};
+
+
+function runCamera(stream) {
+    console.log("initialize the camera");
+    var video = document.querySelector("video");
+    video.width = video.offsetWidth;
+    video.onloadedmetadata = function() {
+        video.play();
+    };
+    video.srcObject = stream;
+}
+
+
+function sendData(data) {
+    var xhr = new XMLHttpRequest();
+    var boundary = "youarenotsupposedtolookatthis";
+    var formData = new FormData();
+    formData.append("selfie", new Blob([data]), "selfie.jpeg");
+    fetch("/publish/", {
+        method: "POST",
+        body: formData
+    }).then(function(response) {
+        if (response.status !== 200) {
+            console.log("something went wrong sending the data: " + response.status);
+        } else {
+            console.log("photo was sent successfully");
+        }
+        cancelPhoto();
+    }).catch(function(err) {
+        console.log("something went wrong connecting to server: " + err);
+        cancelPhoto();
+    });
+}
+
+
+function cancelPhoto() {
+    console.log("cancel photo");
+    var canvas = document.querySelector("canvas");
+    var context = canvas.getContext("2d");
+    context.clearRect(0, 0, canvas.width, canvas.height);
+    countdown.stop();
+}
+
+
+function updateSendCountdown() {
+    console.log("deleting photo in " + countdown.seconds + " seconds");
+}
+
+
+function isBlank(canvas) {
+    var blank = document.createElement("canvas");
+    blank.width = canvas.width;
+    blank.height = canvas.height;
+    return canvas.toDataURL() == blank.toDataURL();
+}
+
+
+function sendPhoto() {
+    console.log("send photo");
+    countdown.stop();
+    var canvas = document.querySelector("canvas");
+    if (isBlank(canvas)) {
+        console.log("cowardly refuse to send a blank image.")
+        return;
+    }
+    return sendData(canvas.toDataURL("image/jpeg"));
+}
+
+
+function takePhoto() {
+    console.log("take photo");
+    var video = document.querySelector("video");
+    var canvas = document.querySelector("canvas");
+    var tmpCanvas = document.createElement("canvas");
+
+    tmpCanvas.width = video.offsetWidth;
+    tmpCanvas.height = video.offsetHeight;
+
+    var tmpContext = tmpCanvas.getContext("2d");
+    var tmpRatio = (tmpCanvas.height / tmpCanvas.width);
+    tmpContext.drawImage(video, 0, 0, video.offsetWidth, video.offsetHeight);
+
+    canvas.width = canvas.offsetWidth;
+    canvas.height = canvas.offsetHeight;
+    canvas.style.height = parseInt(canvas.offsetWidth * tmpRatio);
+    var context = canvas.getContext("2d");
+    var scale = canvas.width / tmpCanvas.width;
+    context.scale(scale, scale);
+    context.drawImage(tmpCanvas, 0, 0);
+    countdown.start(5, cancelPhoto, updateSendCountdown);
+}
+
+
+function initCamera() {
+    console.log("request camera permission");
+    var videoObj = {
+        "video": {
+            width: 800,
+            height: 600
+        },
+        "audio": false
+    };
+
+    navigator.mediaDevices.getUserMedia(videoObj).then(function(stream) {
+        runCamera(stream);
+    }).catch(function(err) {
+        console.log("unable to open camera");
+        console.log(err);
+    });
+}
+

+ 235 - 0
toot-my-t-shirt

@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""toot-my-t-shirt
+
+Copyright 2018 Davide Alberani <da@mimante.net>
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+import os
+import base64
+import logging
+import tempfile
+import datetime
+
+import tornado.httpserver
+import tornado.ioloop
+from tornado.options import define, options
+import tornado.web
+from tornado import gen, escape
+
+from mastodon import Mastodon
+
+
+API_VERSION = '1.0'
+
+
+class Socialite:
+    def __init__(self, options):
+        self.options = options
+        self.init()
+
+    with_mastodon = property(lambda self: self.options.mastodon_token and
+                             self.options.mastodon_api_url)
+
+    with_store = property(lambda self: bool(self.options.store_dir))
+
+    def init(self):
+        self.mastodon = None
+        if self.with_store:
+            if not os.path.isdir(self.options.store_dir):
+                os.makedirs(self.options.store_dir)
+        if self.with_mastodon:
+            self.mastodon = Mastodon(access_token=self.options.mastodon_token,
+                                     api_base_url=self.options.mastodon_api_url)
+
+    def post_image(self, img, mime_type='image/jpeg', message=None, description=None):
+        if message is None:
+            message = self.options.default_message
+        if description is None:
+            description = self.options.default_image_description
+        if self.with_store:
+            self.store_image(img, mime_type, message, description)
+        if self.with_mastodon:
+            self.mastodon_post_image(img, mime_type, message, description)
+
+    def mastodon_post_image(self, img, mime_type, message, description):
+        mdict = self.mastodon.media_post(media_file=img, mime_type=mime_type, description=description)
+        media_id = mdict['id']
+        self.mastodon.status_post(status=message, media_ids=[media_id])
+
+    def store_image(self, img, mime_type, message, description):
+        suffix = '.jpg'
+        if mime_type:
+            ms = mime_type.split('/', 1)
+            if len(ms) == 2 and ms[1]:
+                suffix = '.' + ms[1]
+        prefix = str(datetime.datetime.now()).replace(' ', 'T') + '-'
+        fd, fname = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=self.options.store_dir)
+        print(fd, fname)
+        os.write(fd, img)
+        os.close(fd)
+        txt_fname = '%s.info' % fname
+        with open(txt_fname, 'w') as tfd:
+            tfd.write('message: %s\n' % message or '')
+            tfd.write('description: %s\n' % description or '')
+
+
+class BaseException(Exception):
+    """Base class for toot-my-t-shirt custom exceptions.
+
+    :param message: text message
+    :type message: str
+    :param status: numeric http status code
+    :type status: int"""
+    def __init__(self, message, status=400):
+        super(BaseException, self).__init__(message)
+        self.message = message
+        self.status = status
+
+
+class InputException(BaseException):
+    """Exception raised by errors in input handling."""
+    pass
+
+
+class BaseHandler(tornado.web.RequestHandler):
+    """Base class for request handlers."""
+
+    # A property to access the first value of each argument.
+    arguments = property(lambda self: dict([(k, v[0].decode('utf-8'))
+        for k, v in self.request.arguments.items()]))
+
+    @property
+    def json_body(self):
+        """Return a dictionary from a JSON body.
+
+        :returns: a copy of the body arguments
+        :rtype: dict"""
+        return escape.json_decode(self.request.body or '{}')
+
+    def write_error(self, status_code, **kwargs):
+        """Default error handler."""
+        if isinstance(kwargs.get('exc_info', (None, None))[1], BaseException):
+            exc = kwargs['exc_info'][1]
+            status_code = exc.status
+            message = exc.message
+        else:
+            message = 'internal error'
+        self.build_error(message, status=status_code)
+
+    def is_api(self):
+        """Return True if the path is from an API call."""
+        return self.request.path.startswith('/v%s' % API_VERSION)
+
+    def initialize(self, **kwargs):
+        """Add every passed (key, value) as attributes of the instance."""
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+
+    def build_error(self, message='', status=400):
+        """Build and write an error message.
+
+        :param message: textual message
+        :type message: str
+        :param status: HTTP status code
+        :type status: int
+        """
+        self.set_status(status)
+        self.write({'error': True, 'message': message})
+
+
+class RootHandler(BaseHandler):
+    """Handler for the / path."""
+    app_path = os.path.join(os.path.dirname(__file__), "static")
+
+    @gen.coroutine
+    def get(self, *args, **kwargs):
+        # serve the ./static/index.html file
+        with open(self.app_path + "/index.html", 'r') as fd:
+            self.write(fd.read())
+
+
+class PublishHandler(BaseHandler):
+    @gen.coroutine
+    def post(self, **kwargs):
+        reply = {'success': True}
+        for info in self.request.files['selfie']:
+            _, content_type = info['filename'], info['content_type']
+            body = info['body']
+            b64_image = body.split(b',')[1]
+            image = base64.decodestring(b64_image)
+            with open('/tmp/selfie.jpeg', 'wb') as fd:
+                fd.write(image)
+            self.socialite.post_image(image)
+        self.write(reply)
+
+
+def run():
+    """Run the Tornado web application."""
+    # command line arguments; can also be written in a configuration file,
+    # specified with the --config argument.
+    define("port", default=9000, help="listen on the given port", type=int)
+    define("address", default='', help="bind the server at the given address", type=str)
+
+    define("default-message", help="Default message", type=str)
+    define("default-image-description", help="Default image description", type=str)
+
+    define("mastodon-token", help="Mastodon token", type=str)
+    define("mastodon-api-url", help="Mastodon API URL", type=str)
+
+    define("store-dir", help="store images in this directory", type=str)
+
+    define("ssl_cert", default=os.path.join(os.path.dirname(__file__), 'ssl', 'sb-cert.pem'),
+            help="specify the SSL certificate to use for secure connections")
+    define("ssl_key", default=os.path.join(os.path.dirname(__file__), 'ssl', 'sb-cert.key'),
+            help="specify the SSL private key to use for secure connections")
+
+    define("debug", default=False, help="run in debug mode")
+    define("config", help="read configuration file",
+            callback=lambda path: tornado.options.parse_config_file(path, final=False))
+    tornado.options.parse_command_line()
+
+    logger = logging.getLogger()
+    logger.setLevel(logging.INFO)
+    if options.debug:
+        logger.setLevel(logging.DEBUG)
+
+    ssl_options = {}
+    if os.path.isfile(options.ssl_key) and os.path.isfile(options.ssl_cert):
+        ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
+
+    socialite = Socialite(options)
+    init_params = dict(global_options=options, socialite=socialite)
+
+    _publish_path = r"/publish/?"
+    application = tornado.web.Application([
+            (_publish_path, PublishHandler, init_params),
+            (r'/v%s%s' % (API_VERSION, _publish_path), PublishHandler, init_params),
+            (r"/(?:index.html)?", RootHandler, init_params),
+            (r'/?(.*)', tornado.web.StaticFileHandler, {"path": "static"})
+        ],
+        static_path=os.path.join(os.path.dirname(__file__), "static"),
+        debug=options.debug)
+    http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options or None)
+    logger.info('Start serving on %s://%s:%d', 'https' if ssl_options else 'http',
+                                                 options.address if options.address else '127.0.0.1',
+                                                 options.port)
+    http_server.listen(options.port, options.address)
+    tornado.ioloop.IOLoop.instance().start()
+
+
+if __name__ == '__main__':
+    try:
+        run()
+    except KeyboardInterrupt:
+        print('Server stopped')