From 00c411d33f5600b5c764321f90fa526d57ab77d7 Mon Sep 17 00:00:00 2001 From: Davide Alberani Date: Wed, 28 Nov 2018 21:55:54 +0100 Subject: [PATCH] initial release --- LICENSE.txt | 201 ++++++++++++++++++++++++++++++++++++++ README.md | 27 ++++++ ssl/sb-cert.key | 27 ++++++ ssl/sb-cert.pem | 19 ++++ ssl/sb-root-ca.key | 27 ++++++ ssl/sb-root-ca.pem | 21 ++++ ssl/sb-root-ca.srl | 1 + static/css/sb.css | 19 ++++ static/index.html | 25 +++++ static/js/sb.js | 160 ++++++++++++++++++++++++++++++ toot-my-t-shirt | 235 +++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 762 insertions(+) create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 ssl/sb-cert.key create mode 100644 ssl/sb-cert.pem create mode 100644 ssl/sb-root-ca.key create mode 100644 ssl/sb-root-ca.pem create mode 100644 ssl/sb-root-ca.srl create mode 100644 static/css/sb.css create mode 100644 static/index.html create mode 100644 static/js/sb.js create mode 100755 toot-my-t-shirt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..490c08f --- /dev/null +++ b/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 + +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. + diff --git a/ssl/sb-cert.key b/ssl/sb-cert.key new file mode 100644 index 0000000..605af25 --- /dev/null +++ b/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----- diff --git a/ssl/sb-cert.pem b/ssl/sb-cert.pem new file mode 100644 index 0000000..c3af34c --- /dev/null +++ b/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----- diff --git a/ssl/sb-root-ca.key b/ssl/sb-root-ca.key new file mode 100644 index 0000000..61ab92c --- /dev/null +++ b/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----- diff --git a/ssl/sb-root-ca.pem b/ssl/sb-root-ca.pem new file mode 100644 index 0000000..73d894a --- /dev/null +++ b/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----- diff --git a/ssl/sb-root-ca.srl b/ssl/sb-root-ca.srl new file mode 100644 index 0000000..86f9032 --- /dev/null +++ b/ssl/sb-root-ca.srl @@ -0,0 +1 @@ +BF71980112A2E683 diff --git a/static/css/sb.css b/static/css/sb.css new file mode 100644 index 0000000..a64345a --- /dev/null +++ b/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; +} diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..c1f1a67 --- /dev/null +++ b/static/index.html @@ -0,0 +1,25 @@ + + + + + toot-my-t-shirt + + + + + +
+

toot-my-t-shirt

+
+ +
+
+ +
+ + + + +
+ + diff --git a/static/js/sb.js b/static/js/sb.js new file mode 100644 index 0000000..6136fb5 --- /dev/null +++ b/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); + }); +} + diff --git a/toot-my-t-shirt b/toot-my-t-shirt new file mode 100755 index 0000000..fa210ac --- /dev/null +++ b/toot-my-t-shirt @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""toot-my-t-shirt + +Copyright 2018 Davide Alberani + +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')