├── .env.sample ├── .gitignore ├── LICENSE.md ├── README.md ├── application.go ├── config ├── achievements.json ├── base.json ├── config.go ├── events.json ├── rooms │ ├── arena.json │ ├── auditorium.json │ ├── bronze.json │ ├── coffee_shop.json │ ├── gold.json │ ├── home.json │ ├── left_field.json │ ├── mall.json │ ├── misti.json │ ├── nightclub.json │ ├── nonprofits.json │ ├── personal.json │ ├── plat.json │ ├── plat_area.json │ ├── plaza.json │ ├── right_field.json │ └── silver.json └── sponsors.json ├── controllers └── room.go ├── database.md ├── db ├── connector.go ├── doc.go ├── models │ ├── achievements.go │ ├── character.go │ ├── element.go │ ├── event.go │ ├── hallway.go │ ├── location.go │ ├── log.go │ ├── message.go │ ├── project.go │ ├── queue_subscriber.go │ ├── room.go │ ├── settings.go │ ├── song.go │ └── sponsor.go └── reset.go ├── go.mod ├── go.sum ├── img └── banner.png ├── server ├── router.go └── server.go ├── socket ├── client.go ├── hub.go ├── message.go ├── packet │ ├── README.md │ ├── achievements.go │ ├── add_email.go │ ├── base.go │ ├── chat.go │ ├── dance.go │ ├── element_add.go │ ├── element_delete.go │ ├── element_toggle.go │ ├── element_update.go │ ├── email_code.go │ ├── error.go │ ├── event.go │ ├── friend_request.go │ ├── friend_update.go │ ├── get_achievements.go │ ├── get_current_song.go │ ├── get_map.go │ ├── get_messages.go │ ├── get_songs.go │ ├── get_sponsor.go │ ├── hallway_add.go │ ├── hallway_delete.go │ ├── hallway_update.go │ ├── init.go │ ├── join.go │ ├── jukebox_warning.go │ ├── leave.go │ ├── map.go │ ├── message.go │ ├── messages.go │ ├── move.go │ ├── notification.go │ ├── parse.go │ ├── play_song.go │ ├── project_form.go │ ├── queue_join.go │ ├── queue_remove.go │ ├── queue_subscribe.go │ ├── queue_unsubscribe.go │ ├── queue_update_hacker.go │ ├── queue_update_sponsor.go │ ├── register.go │ ├── report.go │ ├── room_add.go │ ├── settings.go │ ├── song.go │ ├── songs.go │ ├── sponsor.go │ ├── status.go │ ├── teleport.go │ ├── update_map.go │ ├── update_sponsor.go │ └── wardrobe_change.go └── serve.go └── utils ├── bind.go ├── chat.go └── email.go /.env.sample: -------------------------------------------------------------------------------- 1 | AWS_REGION=us-east-1 2 | AWS_ACCESS_KEY_ID= 3 | AWS_SECRET_ACCESS_KEY= 4 | JWT_SECRET= 5 | SLACK_WEBHOOK= 6 | TWILIO_ACCOUNT_SID= 7 | TWILIO_AUTH_TOKEN= 8 | TWITTER_API_KEY= 9 | YOUTUBE_API_KEY= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # misc 2 | .DS_Store 3 | *.swp 4 | .env 5 | config/sponsors.json 6 | 7 | # binary 8 | bin/ 9 | playground 10 | playground.exe 11 | 12 | # Elastic Beanstalk Files 13 | .elasticbeanstalk/* 14 | !.elasticbeanstalk/*.cfg.yml 15 | !.elasticbeanstalk/*.global.yml 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 HackMIT 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # playground 2 | 3 | _Making the HackMIT Club Penguin 🐧 dream come true._ 4 | 5 | [![Gitter](https://badges.gitter.im/HackMIT/playground.svg)](https://gitter.im/HackMIT/playground?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 6 | 7 | ![Dabbing in Playground](img/banner.png) 8 | 9 | ## Setup 10 | 11 | Make sure you have Go installed. If you don't have Go installed, [click here](https://golang.org/doc/install). (If you're on macOS, I recommend using [Homebrew](https://brew.sh)) 12 | 13 | ### Start the database 14 | 15 | Make sure Docker is installed, and that the Docker daemon is running. Then, run the following command to start an ephemeral database in the background: 16 | 17 | ``` 18 | docker run -dp 6379:6379 --name playground-db redis:latest 19 | ``` 20 | 21 | ### Set up secrets 22 | 23 | You'll need our secrets file. If you want to use your own secrets, copy `.env.sample` to `.env` and paste yours in there. If you need to get the HackMIT ones, message Jack. 24 | 25 | ### Build 26 | 27 | To build the project, just run the following command: 28 | 29 | ``` 30 | go build . 31 | ``` 32 | 33 | ### Run 34 | 35 | To run the project, you can then just run the binary: 36 | 37 | ``` 38 | ./playground -reset 39 | ``` 40 | 41 | Use the `-reset` flag the first time you run Playground in order to reset the database to its initial state. After you do that once, you don't have to use the flag anymore, unless you want to wipe everything. 42 | 43 | ### Run the frontend project 44 | 45 | Check out the [playground-frontend](https://github.com/hackmit/playground-frontend) repo for more details about how to set up the user-facing side of this project. 46 | 47 | ## Contributing 48 | 49 | In the coming weeks, we'll be cleaning up the project to a point where it isn't nearly as embarrassing as it is now. If you find any bad coding practices (spoiler: you will), we apologize — much of this project was completed at the last minute. Once we have a chance to clean everything up, we should be ready to receive some contributions! 50 | 51 | ## FAQs 52 | 53 | ### Can I use this for my own event? 54 | 55 | In the coming months, we plan to clean up this project and get it to a point where you can run an event by just adjusting some config files! This won't be ready for a little while though — stay tuned for more info. 56 | 57 | ### Why did you build your own virtual platform? 58 | 59 | Building it was fun! Also, we didn't want to pay for one, and we didn't love the options that were available back in April, when we decided to take on the project. 60 | 61 | ### How can I deploy this? 62 | 63 | We used AWS Elastic Beanstalk during HackMIT in order to handle the load from thousands of concurrently connected users — our project is set up to have multiple ingest servers running in parallel. We'll publish more details about this soon. 64 | 65 | ### I have another question! 66 | 67 | Feel free to ask us on [Gitter](https://gitter.im/HackMIT/playground)! You can also email us at team@hackmit.org. 68 | 69 | ## License 70 | 71 | Playground is available under the MIT license. See the LICENSE file for more details. 72 | 73 | ## Credits 74 | 75 | This project wouldn't have been possible without our supportive and energetic team and the greater open source community. 76 | 77 | ### Dev team 78 | 79 | - [Jack Cook](https://jackcook.com) (_HackMIT co-director, project lead_) 80 | - [Julia Gonik](https://linkedin.com/in/julia-gonik-44813917a) (_Jukebox, workshop attendance_) 81 | - [Natalie Huang](https://linkedin.com/in/natalie-huang-09bba6178) (_A billion last-minute fixes and improvements_) 82 | - [Jianna Liu](https://linkedin.com/in/jianna-liu-90747413b) (_Character profiles_) 83 | - [Kat Liu](https://linkedin.com/in/kaxili) (_Settings, Twitter integration_) 84 | - [Mindy Long](https://mlong93.github.io) (_Chat, account creation, world map_) 85 | - [Michael Lu](https://linkedin.com/in/michael-lee-lu) (_Hacker feedback_) 86 | - [Zoë Marschner](http://zoemarschner.com) (_Three.js wizard 🧙‍♀️_) 87 | - [Shirlyn Prabahar](https://github.com/pshirlyn) (_[Quill](https://github.com/techx/quill) SSO_) 88 | - [Hillary Tamirepi](https://linkedin.com/in/hillary-tamirepi-937660175) (_Playground explorer_) 89 | - [Allen Wang](https://github.com/AllenWang314) (_Sponsor panel, hacker queue_) 90 | - [Eva Yi Xie](https://linkedin.com/in/eva-yi-xie1128) (_Pop-up designs_) 91 | 92 | ### Design team 93 | 94 | - [Angela Zhang](https://www.linkedin.com/in/awzhang) (_HackMIT design lead_) 95 | - [Soomin Chun](https://linkedin.com/in/soomin-chun-95889b173) (_Town square, nightclub, stadium_) 96 | - [Sean Knight](https://knightsean00.github.io) (_Character selector, sponsor areas/buildings_) 97 | - [Savannah Liu](https://linkedin.com/in/savannah-liu-291674197) (_Town square, character profiles, hacker arena_) 98 | - [Gary Nguyen](https://linkedin.com/in/gary-nguyen-mit) (_Non-profit village_) 99 | - [Aliza Rabinovitz](https://linkedin.com/in/alizarabinovitz) (_Settings, achievements, town square 2_) 100 | - [Anna Sun](https://linkedin.com/in/annasun19) (_Chat, coffee shop, town square 2, stadium interior_) 101 | - [Eva Yi Xie](https://linkedin.com/in/eva-yi-xie1128) (_Icons, pop-ups_) 102 | - Jessica Xu (_Icons_) 103 | 104 | ### Resources 105 | 106 | - [“Done For You” sound effect](https://notificationsounds.com/notification-sounds/done-for-you-612), Notification Sounds 107 | - [“Beaver” 3D asset](https://poly.google.com/view/fwtA7VLrXPr), Poly by Google 108 | - [Original character model](http://quaternius.com), Quaternius 109 | - [Several icons](http://iconmonstr.com), iconmonstr 110 | - [Many open source frameworks](https://github.com/hackmit/playground/blob/master/go.mod) 111 | -------------------------------------------------------------------------------- /application.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/techx/playground/config" 9 | "github.com/techx/playground/db" 10 | "github.com/techx/playground/server" 11 | ) 12 | 13 | func main() { 14 | environment := flag.String("e", "dev", "") 15 | port := os.Getenv("PORT") 16 | if port == "" { 17 | port = "5000" 18 | } 19 | reset := flag.Bool("reset", false, "Resets the database") 20 | 21 | flag.Usage = func() { 22 | fmt.Println("Usage: server -e {mode} -p {port}") 23 | os.Exit(1) 24 | } 25 | 26 | flag.Parse() 27 | 28 | config.Init(*environment) 29 | db.Init(*reset) 30 | server.Init(port) 31 | } 32 | -------------------------------------------------------------------------------- /config/achievements.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Company Tour", 4 | "description": "We have a lot of cool sponsors, and we'd love for you to meet them! Each company has its own lobby and room specially designed for you to interact and learn more about them. Visit the rooms of at least three companies and at least one non-profit in order to get your badge!" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "achievements": { 3 | "num_workshops": 2, 4 | "num_mini_events": 1, 5 | "num_sponsors": 4, 6 | "num_friends": 5 7 | }, 8 | "db": { 9 | "addr": "localhost:6379", 10 | "password": "", 11 | "db": 0 12 | }, 13 | "feedback_open": 1600621200, 14 | "tim": { 15 | "allowed_rooms": [ 16 | "home", 17 | "plaza", 18 | "plat_area", 19 | "left_field", 20 | "right_field", 21 | "nonprofits" 22 | ], 23 | "action_probs": { 24 | "walk": 0.5, 25 | "talk": 0.25, 26 | "teleport": 0.25 27 | }, 28 | "bio": "hi! i’m tim the beaver. super excited to be at hackmit again this year! had to fight some kangaroos and elephants to grab my position as mit’s mascot, but getting to attend blueprint makes it all worth it :))", 29 | "chat_lines": [ 30 | "call me speedy boi tim! *zoom*", 31 | "pls come to my 107th birthday next year!", 32 | "lmk if you see my cousins Flat Tim and Sloanie Tim!", 33 | "mens et paw", 34 | "like hackers, beavers are very much nocturnal!", 35 | "call me nature’s engineer ;)", 36 | "did u know i used to be called Bucky, Chipper, and Eager" 37 | ] 38 | }, 39 | "twilio": { 40 | "from_phone_number": "+16172957468" 41 | }, 42 | "webpush": { 43 | "public_key": "BB584OYloNsGGb3p4lIv4hb0J4ZPgd3WlqhEVA_e9DuZsJ2XMaBOplUlnPIAXSna8gDchIHtF9_MNzMfd9jTEtQ", 44 | "private_key": "UHZ4QPdkoq6o3pCvbIRNh4NWBJ_lYAFXBIAF78ETLH0" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/joho/godotenv" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | type Secret string 12 | 13 | const ( 14 | TwilioAccountSID Secret = "TWILIO_ACCOUNT_SID" 15 | TwilioAuthToken = "TWILIO_AUTH_TOKEN" 16 | YouTubeKey = "YOUTUBE_API_KEY" 17 | ) 18 | 19 | var config *viper.Viper 20 | 21 | func Init(env string) { 22 | var err error 23 | 24 | // Load environment variables 25 | godotenv.Load() 26 | 27 | // Load config from config/base.json 28 | config = viper.New() 29 | config.SetConfigName("base") 30 | config.AddConfigPath(".") 31 | config.AddConfigPath("config") 32 | err = config.ReadInConfig() 33 | 34 | if err != nil { 35 | log.Println(err) 36 | log.Fatal("Unable to process base config file. Make sure the " + 37 | "file contains valid JSON and is in the correct " + 38 | "location.") 39 | } 40 | 41 | // Allow for environment-specific configs (e.g. dev.json or prod.json) 42 | config.SetConfigName(env) 43 | config.MergeInConfig() 44 | } 45 | 46 | func GetConfig() *viper.Viper { 47 | return config 48 | } 49 | 50 | func GetSecret(key Secret) string { 51 | return os.Getenv(string(key)) 52 | } 53 | -------------------------------------------------------------------------------- /config/events.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Opening Ceremony", 4 | "start_time": "2020-09-18T21:00:00-0400", 5 | "duration": 60, 6 | "url": "https://go.hackmit.org/opening" 7 | }, 8 | { 9 | "name": "Team Formation", 10 | "start_time": "2020-09-18T22:00:00-0400", 11 | "duration": 60, 12 | "url": "https://go.hackmit.org/discord" 13 | }, 14 | { 15 | "name": "InterSystems: Data in the Cloud with InterSystems's IRIS Platform", 16 | "start_time": "2020-09-18T23:00:00-0400", 17 | "duration": 45, 18 | "url": "https://go.hackmit.org/workshop", 19 | "type": "workshop" 20 | }, 21 | { 22 | "name": "Beginner Workshop (part 1)", 23 | "start_time": "2020-09-19T00:00:00-0400", 24 | "duration": 60, 25 | "url": "https://go.hackmit.org/workshop", 26 | "type": "workshop" 27 | }, 28 | { 29 | "name": "Mini Event: Speed Friending", 30 | "start_time": "2020-09-19T01:00:00-0400", 31 | "duration": 60, 32 | "url": "https://go.hackmit.org/mini-event", 33 | "type": "mini_event" 34 | }, 35 | { 36 | "name": "Beginner Workshop (part 2)", 37 | "start_time": "2020-09-19T10:00:00-0400", 38 | "duration": 60, 39 | "url": "https://go.hackmit.org/workshop", 40 | "type": "workshop" 41 | }, 42 | { 43 | "name": "Beginner Workshop (part 3)", 44 | "start_time": "2020-09-19T11:15:00-0400", 45 | "duration": 60, 46 | "url": "https://go.hackmit.org/workshop", 47 | "type": "workshop" 48 | }, 49 | { 50 | "name": "CMT: Detecting Vehicle Crashes with Smartphones", 51 | "start_time": "2020-09-19T12:30:00-0400", 52 | "duration": 45, 53 | "url": "https://go.hackmit.org/workshop", 54 | "type": "workshop" 55 | }, 56 | { 57 | "name": "IBM: Build a COVID-19 Crisis Communication Chatbot Using IBM Cloud Technologies", 58 | "start_time": "2020-09-19T13:30:00-0400", 59 | "duration": 45, 60 | "url": "https://go.hackmit.org/workshop", 61 | "type": "workshop" 62 | }, 63 | { 64 | "name": "Yext: An Intro to REST APIs", 65 | "start_time": "2020-09-19T14:30:00-0400", 66 | "duration": 30, 67 | "url": "https://go.hackmit.org/workshop", 68 | "type": "workshop" 69 | }, 70 | { 71 | "name": "Facebook: Build Web Apps with React", 72 | "start_time": "2020-09-19T15:15:00-0400", 73 | "duration": 30, 74 | "url": "https://go.hackmit.org/workshop", 75 | "type": "workshop" 76 | }, 77 | { 78 | "name": "Mini Event: TikTok Dance Workshop", 79 | "start_time": "2020-09-19T16:00:00-0400", 80 | "duration": 30, 81 | "url": "https://go.hackmit.org/mini-event", 82 | "type": "mini_event" 83 | }, 84 | { 85 | "name": "IBM: The Role of Artificial Intelligence in Responding to the Pandemic", 86 | "start_time": "2020-09-19T16:45:00-0400", 87 | "duration": 30, 88 | "url": "https://go.hackmit.org/workshop", 89 | "type": "workshop" 90 | }, 91 | { 92 | "name": "Mini Event: Abs with Jenny", 93 | "start_time": "2020-09-19T17:30:00-0400", 94 | "duration": 30, 95 | "url": "https://go.hackmit.org/mini-event", 96 | "type": "mini_event" 97 | }, 98 | { 99 | "name": "Peer Expo", 100 | "start_time": "2020-09-19T18:00:00-0400", 101 | "duration": 60, 102 | "url": "room:arena:education" 103 | }, 104 | { 105 | "name": "Hackathon Organizer Meetup", 106 | "start_time": "2020-09-19T19:00:00-0400", 107 | "duration": 60, 108 | "url": "https://go.hackmit.org/hackathon-organizer" 109 | }, 110 | { 111 | "name": "Fireside Chat with Aileen Lee", 112 | "start_time": "2020-09-19T20:00:00-0400", 113 | "duration": 45, 114 | "url": "https://go.hackmit.org/fireside1", 115 | "type": "workshop" 116 | }, 117 | { 118 | "name": "Fireside Chat with Linda Avey", 119 | "start_time": "2020-09-19T21:00:00-0400", 120 | "duration": 45, 121 | "url": "https://go.hackmit.org/fireside2", 122 | "type": "workshop" 123 | }, 124 | { 125 | "name": "Fireside Chat with Tom Preston-Werner", 126 | "start_time": "2020-09-19T22:00:00-0400", 127 | "duration": 45, 128 | "url": "https://go.hackmit.org/fireside3", 129 | "type": "workshop" 130 | }, 131 | { 132 | "name": "HackMIT Tech Talk", 133 | "start_time": "2020-09-19T23:00:00-0400", 134 | "duration": 60, 135 | "url": "https://go.hackmit.org/workshop", 136 | "type": "workshop" 137 | }, 138 | { 139 | "name": "Mini Event: Trivia", 140 | "start_time": "2020-09-19T23:00:00-0400", 141 | "duration": 60, 142 | "url": "https://go.hackmit.org/mini-event", 143 | "type": "mini_event" 144 | }, 145 | { 146 | "name": "Mini Event: Typeracer", 147 | "start_time": "2020-09-20T00:30:00-0400", 148 | "duration": 30, 149 | "url": "https://go.hackmit.org/mini-event", 150 | "type": "mini_event" 151 | }, 152 | { 153 | "name": "PROJECT SUBMISSION IS DUE AT 10AM EDT", 154 | "start_time": "2020-09-20T01:00:00-0400", 155 | "duration": 540, 156 | "url": "https://spectacle.hackmit.org" 157 | }, 158 | { 159 | "name": "Panel Judging", 160 | "start_time": "2020-09-20T13:15:00-0400", 161 | "duration": 75, 162 | "url": "https://go.hackmit.org/closing" 163 | }, 164 | { 165 | "name": "Closing Ceremony", 166 | "start_time": "2020-09-20T14:30:00-0400", 167 | "duration": 45, 168 | "url": "https://go.hackmit.org/closing" 169 | } 170 | ] 171 | -------------------------------------------------------------------------------- /config/rooms/arena.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "arena_.svg", 3 | "corners": "0.8476,0.5345;0.3903,0.9980;0.1506,0.7574;0.6045,0.2682", 4 | "elements": [ 5 | { 6 | "x": 0.39, 7 | "y": 0.5334, 8 | "width": 0.3885, 9 | "path": "long_couch.svg" 10 | }, 11 | { 12 | "x": 0.1868, 13 | "y": 0.6919, 14 | "width": 0.029, 15 | "path": "_plant.svg" 16 | }, 17 | { 18 | "x": 0.7337, 19 | "y": 0.408, 20 | "width": 0.1552, 21 | "path": "_counter.svg" 22 | }, 23 | { 24 | "x": 0.6201, 25 | "y": 0.2651, 26 | "width": 0.0595, 27 | "path": "_cooler.svg" 28 | }, 29 | { 30 | "x": 0.6105, 31 | "y": 0.5335, 32 | "width": 0.0475, 33 | "path": "_table.svg" 34 | }, 35 | { 36 | "x": 0.4805, 37 | "y": 0.6565, 38 | "width": 0.0475, 39 | "path": "_table.svg" 40 | }, 41 | { 42 | "x": 0.3498, 43 | "y": 0.7978, 44 | "width": 0.0475, 45 | "path": "_table.svg" 46 | } 47 | ], 48 | "hallways": [] 49 | } 50 | -------------------------------------------------------------------------------- /config/rooms/auditorium.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "auditorium.svg", 3 | "corners": "0.1129,0.6226;0.5172,0.2019;0.887,0.5888;0.4815,0.9926", 4 | "elements": [ 5 | { 6 | "x": 0.4909, 7 | "y": 0.5996, 8 | "width": 0.5769, 9 | "path": "auditorium_chairs.svg" 10 | }, 11 | { 12 | "x": 0.5246, 13 | "y": 0.2113, 14 | "width": 0.0324, 15 | "path": "trash_recycling_flipped.svg" 16 | }, 17 | { 18 | "x": 0.608, 19 | "y": 0.2101, 20 | "width": 0.0976, 21 | "path": "auditorium_whiteboard.svg", 22 | "action": 6 23 | } 24 | ], 25 | "hallways": [ 26 | { 27 | "x": 0.8214, 28 | "y": 0.5172, 29 | "radius": 0.0826, 30 | "to": "plaza", 31 | "toX": 0.8869, 32 | "toY": 0.4579 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /config/rooms/bronze.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": ".svg", 3 | "sponsor": true, 4 | "corners": "0.8041,0.6878;0.4988,0.9951;0.1935,0.6857;0.5023,0.3537", 5 | "elements": [ 6 | { 7 | "x": 0.2699, 8 | "y": 0.524, 9 | "width": 0.0555, 10 | "path": "door_flipped.svg" 11 | }, 12 | { 13 | "x": 0.6163, 14 | "y": 0.5395, 15 | "width": 0.1922, 16 | "path": "desk_.svg" 17 | }, 18 | { 19 | "x": 0.4787, 20 | "y": 0.3544, 21 | "width": 0.0992, 22 | "path": "tall_shelf_.svg" 23 | }, 24 | { 25 | "x": 0.4007, 26 | "y": 0.7725, 27 | "width": 0.199, 28 | "path": "sofa_back_flipped.svg" 29 | }, 30 | { 31 | "x": 0.5241, 32 | "y": 0.869, 33 | "width": 0.1112, 34 | "path": "small_shelf_.svg" 35 | } 36 | ], 37 | "hallways": [ 38 | { 39 | "x": 0.2767, 40 | "y": 0.6116, 41 | "radius": 0.06, 42 | "to": "left_field", 43 | "toX": 0.5, 44 | "toY": 0.5 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /config/rooms/coffee_shop.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "coffee_shop.svg", 3 | "corners": "0.6323,0.2505;0.8653,0.4911;0.3681,0.9980;0.1350,0.7574", 4 | "elements": [ 5 | { 6 | "x": 0.8055, 7 | "y": 0.4203, 8 | "width": 0.1252, 9 | "path": "coffee_bar.svg" 10 | }, 11 | { 12 | "x": 0.3868, 13 | "y": 0.9271, 14 | "width": 0.0791, 15 | "path": "coffee_shop_table.svg" 16 | }, 17 | { 18 | "x": 0.3416, 19 | "y": 0.8501, 20 | "width": 0.1198, 21 | "path": "coffee_shop_sofa.svg" 22 | }, 23 | { 24 | "x": 0.7078, 25 | "y": 0.4424, 26 | "width": 0.1826, 27 | "path": "coffee_shop_counter.svg" 28 | }, 29 | { 30 | "x": 0.5339, 31 | "y": 0.338, 32 | "width": 0.0568, 33 | "path": "trash_recycling.svg" 34 | }, 35 | { 36 | "x": 0.4346, 37 | "y": 0.8374, 38 | "width": 0.0256, 39 | "path": "plant_white.svg" 40 | }, 41 | { 42 | "x": 0.4976, 43 | "y": 0.3608, 44 | "width": 0.0256, 45 | "path": "plant_white.svg" 46 | }, 47 | { 48 | "x": 0.2301, 49 | "y": 0.7329, 50 | "width": 0.1018, 51 | "path": "left_table.svg" 52 | }, 53 | { 54 | "x": 0.5008, 55 | "y": 0.7201, 56 | "width": 0.0984, 57 | "path": "lower_table.svg" 58 | }, 59 | { 60 | "x": 0.2577, 61 | "y": 0.477, 62 | "width": 0.1826, 63 | "path": "wall_map.svg", 64 | "action": 2 65 | }, 66 | { 67 | "x": 0.4016, 68 | "y": 0.5534, 69 | "width": 0.0993, 70 | "path": "upper_table.svg" 71 | } 72 | ], 73 | "hallways": [ 74 | { 75 | "x": 0.6637, 76 | "y": 0.283, 77 | "radius": 0.0407, 78 | "to": "plaza", 79 | "toX": 0.2278, 80 | "toY": 0.6469 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /config/rooms/gold.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": ".svg", 3 | "sponsor": true, 4 | "corners": "0.9378,0.7984;0.7396,0.9972;0.5150,0.7697;0.2892,0.9972;0.0922,0.7964;0.5138,0.3619", 5 | "elements": [ 6 | { 7 | "x": 0.2252, 8 | "y": 0.7793, 9 | "width": 0.2029, 10 | "path": "desk_.svg" 11 | }, 12 | { 13 | "x": 0.439, 14 | "y": 0.4095, 15 | "width": 0.0301, 16 | "path": "plant_.svg" 17 | }, 18 | { 19 | "x": 0.3909, 20 | "y": 0.4593, 21 | "width": 0.0846, 22 | "path": "shelf_.svg" 23 | }, 24 | { 25 | "x": 0.9075, 26 | "y": 0.7289, 27 | "width": 0.0319, 28 | "path": "plant_brown.svg" 29 | }, 30 | { 31 | "x": 0.618, 32 | "y": 0.6045, 33 | "width": 0.2041, 34 | "path": "sofa_front.svg" 35 | }, 36 | { 37 | "x": 0.7033, 38 | "y": 0.7129, 39 | "width": 0.1312, 40 | "path": "coffee_table.svg" 41 | }, 42 | { 43 | "x": 0.7973, 44 | "y": 0.8248, 45 | "width": 0.2041, 46 | "path": "sofa_back.svg" 47 | }, 48 | { 49 | "x": 0.7403, 50 | "y": 0.904, 51 | "width": 0.0319, 52 | "path": "plant_brown.svg" 53 | }, 54 | { 55 | "x": 0.5597, 56 | "y": 0.3234, 57 | "width": 0.0555, 58 | "path": "door.svg" 59 | } 60 | ], 61 | "hallways": [ 62 | { 63 | "x": 0.5416, 64 | "y": 0.4214, 65 | "radius": 0.0572, 66 | "to": "plat_area", 67 | "toX": 0.5, 68 | "toY": 0.5 69 | } 70 | ] 71 | } 72 | -------------------------------------------------------------------------------- /config/rooms/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "town_square.svg", 3 | "sponsor": false, 4 | "elements": [ 5 | { 6 | "x": 0.5032, 7 | "y": 0.2671, 8 | "width": 0.0297, 9 | "path": "tree2.svg" 10 | }, 11 | { 12 | "x": 0.4444, 13 | "y": 0.2136, 14 | "width": 0.0297, 15 | "path": "tree2.svg" 16 | }, 17 | { 18 | "x": 0.5528, 19 | "y": 0.217, 20 | "width": 0.0297, 21 | "path": "tree2.svg" 22 | }, 23 | { 24 | "x": 0.6093, 25 | "y": 0.16, 26 | "width": 0.0297, 27 | "path": "tree2.svg" 28 | }, 29 | { 30 | "x": 0.6639, 31 | "y": 0.0974, 32 | "width": 0.0297, 33 | "path": "tree2.svg" 34 | }, 35 | { 36 | "x": 0.3898, 37 | "y": 0.1475, 38 | "width": 0.0297, 39 | "path": "tree2.svg" 40 | }, 41 | { 42 | "x": 0.3265, 43 | "y": 0.0917, 44 | "width": 0.0297, 45 | "path": "tree2.svg" 46 | }, 47 | { 48 | "x": 0.236, 49 | "y": 0.3089, 50 | "width": 0.3664, 51 | "path": "hacker_arena.svg" 52 | }, 53 | { 54 | "x": 0.05, 55 | "y": 0.455, 56 | "width": 0.0302, 57 | "path": "tree_smol.svg" 58 | }, 59 | { 60 | "x": 0.0704, 61 | "y": 0.4744, 62 | "width": 0.0302, 63 | "path": "tree_smol.svg" 64 | }, 65 | { 66 | "x": 0.0909, 67 | "y": 0.496, 68 | "width": 0.0302, 69 | "path": "tree_smol.svg" 70 | }, 71 | { 72 | "x": 0.11, 73 | "y": 0.5177, 74 | "width": 0.0302, 75 | "path": "tree_smol.svg" 76 | }, 77 | { 78 | "x": 0.7776, 79 | "y": 0.302, 80 | "width": 0.3735, 81 | "path": "shopping_mall.svg" 82 | }, 83 | { 84 | "x": 0.9035, 85 | "y": 0.7513, 86 | "width": 0.0596, 87 | "path": "plaza_sign.svg" 88 | }, 89 | { 90 | "x": 0.0965, 91 | "y": 0.7513, 92 | "width": 0.0713, 93 | "path": "organizations_sign.svg" 94 | }, 95 | { 96 | "x": 0.1738, 97 | "y": 0.6947, 98 | "width": 0.0436, 99 | "path": "billboard.svg" 100 | }, 101 | { 102 | "x": 0.5, 103 | "y": 0.5426, 104 | "width": 0.0906, 105 | "fountain": true 106 | }, 107 | { 108 | "x": 0.5366, 109 | "y": 0.6171, 110 | "width": 0.0436, 111 | "path": "bench.svg" 112 | }, 113 | { 114 | "x": 0.4666, 115 | "y": 0.6252, 116 | "width": 0.0436, 117 | "path": "bench_flipped.svg" 118 | }, 119 | { 120 | "x": 0.8621, 121 | "y": 0.5622, 122 | "width": 0.0433, 123 | "path": "bike_rack_flipped.svg" 124 | }, 125 | { 126 | "x": 0.8386, 127 | "y": 0.7102, 128 | "width": 0.0302, 129 | "path": "recycling_bin.svg" 130 | } 131 | ], 132 | "hallways": [ 133 | { 134 | "x": 0.9214, 135 | "y": 0.9018, 136 | "radius": 0.1294, 137 | "to": "plaza", 138 | "toX": 0.5711, 139 | "toY": 0.0957 140 | }, 141 | { 142 | "x": 0.051, 143 | "y": 0.9313, 144 | "radius": 0.15, 145 | "to": "left_field", 146 | "toX": 0.0239, 147 | "toY": 0.4121 148 | }, 149 | { 150 | "x": 0.3111, 151 | "y": 0.4728, 152 | "radius": 0.0804, 153 | "to": "arena:health", 154 | "toX": 0.2572, 155 | "toY": 0.8132 156 | }, 157 | { 158 | "x": 0.619, 159 | "y": 0.4755, 160 | "radius": 0.0706, 161 | "to": "mall", 162 | "toX": 0.7449, 163 | "toY": 0.7077 164 | } 165 | ] 166 | } 167 | -------------------------------------------------------------------------------- /config/rooms/left_field.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "left_field.svg", 3 | "sponsor": false, 4 | "elements": [ 5 | { 6 | "x": 0.1321, 7 | "y": 0.104, 8 | "width": 0.0558, 9 | "path": "tree.svg" 10 | }, 11 | { 12 | "x": 0.2352, 13 | "y": 0.0495, 14 | "width": 0.0558, 15 | "path": "tree.svg" 16 | }, 17 | { 18 | "x": 0.3478, 19 | "y": 0.1756, 20 | "width": 0.0558, 21 | "path": "tree.svg" 22 | }, 23 | { 24 | "x": 0.5008, 25 | "y": 0.1412, 26 | "width": 0.0558, 27 | "path": "tree.svg" 28 | }, 29 | { 30 | "x": 0.9633, 31 | "y": 0.0782, 32 | "width": 0.0558, 33 | "path": "tree.svg" 34 | }, 35 | { 36 | "x": 0.4177, 37 | "y": 0.3217, 38 | "width": 0.04, 39 | "path": "tree2.svg" 40 | }, 41 | { 42 | "x": 0.0583, 43 | "y": 0.1211, 44 | "width": 0.04, 45 | "path": "tree2.svg" 46 | }, 47 | { 48 | "x": 0.2058, 49 | "y": 0.2128, 50 | "width": 0.04, 51 | "path": "tree2.svg" 52 | }, 53 | { 54 | "x": 0.6578, 55 | "y": 0.104, 56 | "width": 0.04, 57 | "path": "tree2.svg" 58 | }, 59 | { 60 | "x": 0.4923, 61 | "y": 0.6381, 62 | "width": 0.0287, 63 | "path": "tree2.svg" 64 | }, 65 | { 66 | "x": 0.815, 67 | "y": 0.28, 68 | "width": 0.24, 69 | "path": "building_ktbyte.svg" 70 | }, 71 | { 72 | "x": 0.64, 73 | "y": 0.511, 74 | "width": 0.2822, 75 | "path": "building_kodewithklossy.svg" 76 | }, 77 | { 78 | "x": 0.1958, 79 | "y": 0.373, 80 | "width": 0.2, 81 | "path": "building_ieee.svg" 82 | }, 83 | { 84 | "x": 0.38, 85 | "y": 0.51, 86 | "width": 0.21, 87 | "path": "building_beaverworks.svg" 88 | }, 89 | { 90 | "x": 0.9425, 91 | "y": 0.7759, 92 | "width": 0.0558, 93 | "path": "tree.svg" 94 | }, 95 | { 96 | "x": 0.0473, 97 | "y": 0.7816, 98 | "width": 0.0558, 99 | "path": "tree.svg" 100 | }, 101 | { 102 | "x": 0.1892, 103 | "y": 0.879, 104 | "width": 0.0558, 105 | "path": "tree.svg" 106 | }, 107 | { 108 | "x": 0.8798, 109 | "y": 0.8645, 110 | "width": 0.04, 111 | "path": "tree2.svg" 112 | }, 113 | { 114 | "x": 0.5, 115 | "y": 0.8232, 116 | "width": 0.0906, 117 | "fountain": true 118 | }, 119 | { 120 | "x": 0.5663, 121 | "y": 0.9235, 122 | "width": 0.0436, 123 | "path": "bench.svg" 124 | }, 125 | { 126 | "x": 0.4398, 127 | "y": 0.9321, 128 | "width": 0.0436, 129 | "path": "bench_flipped.svg" 130 | }, 131 | { 132 | "x": 0.9773, 133 | "y": 0.4392, 134 | "width": 0.0223, 135 | "path": "street_lamp.svg", 136 | "toggleable": true 137 | }, 138 | { 139 | "x": 0.8831, 140 | "y": 0.5324, 141 | "width": 0.0223, 142 | "path": "street_lamp.svg", 143 | "toggleable": true 144 | }, 145 | { 146 | "x": 0.9773, 147 | "y": 0.4392, 148 | "width": 0.0223, 149 | "path": "street_lamp.svg", 150 | "toggleable": true 151 | }, 152 | { 153 | "x": 0.7913, 154 | "y": 0.6212, 155 | "width": 0.0223, 156 | "path": "street_lamp.svg", 157 | "toggleable": true 158 | }, 159 | { 160 | "x": 0.6648, 161 | "y": 0.753, 162 | "width": 0.0223, 163 | "path": "street_lamp.svg", 164 | "toggleable": true 165 | }, 166 | { 167 | "x": 0.3417, 168 | "y": 0.7659, 169 | "width": 0.0223, 170 | "path": "street_lamp.svg", 171 | "toggleable": true 172 | }, 173 | { 174 | "x": 0.2451, 175 | "y": 0.667, 176 | "width": 0.0223, 177 | "path": "street_lamp.svg", 178 | "toggleable": true 179 | }, 180 | { 181 | "x": 0.1315, 182 | "y": 0.5481, 183 | "width": 0.0223, 184 | "path": "street_lamp.svg", 185 | "toggleable": true 186 | }, 187 | { 188 | "x": 0.5, 189 | "y": 0.9488, 190 | "width": 0.0189, 191 | "path": "recycling_bin.svg" 192 | }, 193 | { 194 | "x": 0.763, 195 | "y": 0.5375, 196 | "width": 0.0213, 197 | "path": "bike_rack_flipped.svg" 198 | } 199 | ], 200 | "hallways": [ 201 | { 202 | "x": 0.9903, 203 | "y": 0.3624, 204 | "radius": 0.0579, 205 | "to": "right_field", 206 | "toX": 0.0305, 207 | "toY": 0.402 208 | }, 209 | { 210 | "x": 0.8792, 211 | "y": 0.4259, 212 | "radius": 0.0512, 213 | "to": "sponsor:ktbyte", 214 | "toX": 0.4961, 215 | "toY": 0.5002 216 | }, 217 | { 218 | "x": 0.6922, 219 | "y": 0.6394, 220 | "radius": 0.0469, 221 | "to": "sponsor:kodewithklossy", 222 | "toX": 0.4961, 223 | "toY": 0.5002 224 | }, 225 | { 226 | "x": 0.3257, 227 | "y": 0.6592, 228 | "radius": 0.0479, 229 | "to": "sponsor:beaverworks", 230 | "toX": 0.4961, 231 | "toY": 0.5002 232 | }, 233 | { 234 | "x": 0.1384, 235 | "y": 0.4778, 236 | "radius": 0.0407, 237 | "to": "sponsor:ieee", 238 | "toX": 0.4961, 239 | "toY": 0.5002 240 | } 241 | ] 242 | } 243 | -------------------------------------------------------------------------------- /config/rooms/mall.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "mall.svg", 3 | "corners": "0.8165,0.7515;0.5701,0.9961;0.1850,0.6036;0.4292,0.3511", 4 | "elements": [ 5 | { 6 | "x": 0.322, 7 | "y": 0.5715, 8 | "width": 0.1486, 9 | "path": "mall_checkout.svg" 10 | }, 11 | { 12 | "x": 0.5647, 13 | "y": 0.3467, 14 | "width": 0.1, 15 | "path": "mall_clothes.svg" 16 | }, 17 | { 18 | "x": 0.4291, 19 | "y": 0.3309, 20 | "width": 0.1, 21 | "path": "mall_fittingroom_closed.svg", 22 | "action": 3 23 | }, 24 | { 25 | "x": 0.3889, 26 | "y": 0.6891, 27 | "width": 0.1, 28 | "path": "mall_plants.svg" 29 | }, 30 | { 31 | "x": 0.7007, 32 | "y": 0.7364, 33 | "width": 0.1, 34 | "path": "mall_stand1.svg" 35 | }, 36 | { 37 | "x": 0.5488, 38 | "y": 0.6857, 39 | "width": 0.153, 40 | "path": "mall_stand2.svg" 41 | } 42 | ], 43 | "hallways": [ 44 | { 45 | "x": 0.7437, 46 | "y": 0.667, 47 | "radius": 0.0754, 48 | "to": "home", 49 | "toX": 0.5973, 50 | "toY": 0.5296 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /config/rooms/misti.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "misti.svg", 3 | "corners": "0.8276,0.6430;0.4747,0.9961;0.1706,0.6864;0.5168,0.3116", 4 | "elements": [ 5 | { 6 | "x": 0.3825, 7 | "y": 0.4865, 8 | "width": 0.1355, 9 | "path": "misti_couch.svg" 10 | }, 11 | { 12 | "x": 0.4436, 13 | "y": 0.8467, 14 | "width": 0.1258, 15 | "path": "misti_table_yellow.svg" 16 | }, 17 | { 18 | "x": 0.6387, 19 | "y": 0.6533, 20 | "width": 0.1258, 21 | "path": "misti_table_yellow.svg" 22 | }, 23 | { 24 | "x": 0.5398, 25 | "y": 0.7507, 26 | "width": 0.1258, 27 | "path": "misti_table_pink.svg" 28 | }, 29 | { 30 | "x": 0.2609, 31 | "y": 0.5997, 32 | "width": 0.1258, 33 | "path": "misti_table_pink_flipped.svg" 34 | }, 35 | { 36 | "x": 0.734, 37 | "y": 0.343, 38 | "width": 0.1486, 39 | "path": "misti_menu.svg", 40 | "action": 5 41 | } 42 | ], 43 | "hallways": [ 44 | { 45 | "x": 0.565, 46 | "y": 0.3786, 47 | "radius": 0.0609, 48 | "to": "plaza", 49 | "toX": 0.3879, 50 | "toY": 0.4226 51 | } 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /config/rooms/nightclub.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "nightclub.svg", 3 | "sponsor": false, 4 | "corners": "0.8152,0.7418;0.5701,0.9961;0.1839,0.6036;0.4325,0.3531", 5 | "elements": [ 6 | { 7 | "x": 0.5121, 8 | "y": 0.7196, 9 | "width": 0.3738, 10 | "path": "glass_cover.svg" 11 | }, 12 | { 13 | "x": 0.343, 14 | "y": 0.4383, 15 | "width": 0.0207, 16 | "path": "cute_dj.svg" 17 | }, 18 | { 19 | "x": 0.3563, 20 | "y": 0.5216, 21 | "width": 0.0781, 22 | "path": "djbooth.svg", 23 | "action": 1 24 | }, 25 | { 26 | "x": 0.5837, 27 | "y": 0.5273, 28 | "width": 0.2146, 29 | "path": "bar_closed.svg", 30 | "toggleable": true 31 | }, 32 | { 33 | "x": 0.427, 34 | "y": 0.2335, 35 | "width": 0.0334, 36 | "path": "speaker.svg" 37 | }, 38 | { 39 | "x": 0.2111, 40 | "y": 0.4557, 41 | "width": 0.0334, 42 | "path": "speaker.svg" 43 | }, 44 | { 45 | "x": 0.5054, 46 | "y": 0.8367, 47 | "width": 0.059, 48 | "path": "table.svg" 49 | }, 50 | { 51 | "x": 0.4436, 52 | "y": 0.7762, 53 | "width": 0.059, 54 | "path": "table.svg" 55 | }, 56 | { 57 | "x": 0.3846, 58 | "y": 0.7136, 59 | "width": 0.059, 60 | "path": "table.svg" 61 | }, 62 | { 63 | "x": 0.4661, 64 | "y": 0.3031, 65 | "width": 0.0136, 66 | "path": "stripper_pole.svg" 67 | } 68 | ], 69 | "hallways": [ 70 | { 71 | "x": 0.7576, 72 | "y": 0.6749, 73 | "radius": 0.0333, 74 | "to": "home", 75 | "toX": 0.925, 76 | "toY": 0.5961 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /config/rooms/nonprofits.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "nonprofits.svg", 3 | "sponsor": false, 4 | "elements": [ 5 | { 6 | "x": 0.3624, 7 | "y": 0.2837, 8 | "width": 0.1303, 9 | "path": "tent_urban_displacement.svg", 10 | "action": 4 11 | }, 12 | { 13 | "x": 0.2111, 14 | "y": 0.117, 15 | "width": 0.1303, 16 | "path": "tent.svg" 17 | }, 18 | { 19 | "x": 0.1194, 20 | "y": 0.3195, 21 | "width": 0.1303, 22 | "path": "tent.svg" 23 | }, 24 | { 25 | "x": 0.2881, 26 | "y": 0.6652, 27 | "width": 0.1303, 28 | "path": "tent_wikimedia.svg", 29 | "action": 4 30 | }, 31 | { 32 | "x": 0.0923, 33 | "y": 0.7652, 34 | "width": 0.1303, 35 | "path": "tent.svg" 36 | }, 37 | { 38 | "x": 0.3462, 39 | "y": 0.8261, 40 | "width": 0.0509, 41 | "path": "tree.svg" 42 | }, 43 | { 44 | "x": 0.1788, 45 | "y": 0.9088, 46 | "width": 0.0509, 47 | "path": "tree.svg" 48 | }, 49 | { 50 | "x": 0.1695, 51 | "y": 0.5841, 52 | "width": 0.0509, 53 | "path": "tree.svg" 54 | }, 55 | { 56 | "x": 0.3933, 57 | "y": 0.0854, 58 | "width": 0.0509, 59 | "path": "tree.svg" 60 | }, 61 | { 62 | "x": 0.0537, 63 | "y": 0.1545, 64 | "width": 0.0509, 65 | "path": "tree.svg" 66 | }, 67 | { 68 | "x": 0.5453, 69 | "y": 0.7705, 70 | "width": 0.0509, 71 | "path": "tree.svg" 72 | }, 73 | { 74 | "x": 0.8104, 75 | "y": 0.9199, 76 | "width": 0.0509, 77 | "path": "tree.svg" 78 | }, 79 | { 80 | "x": 0.8605, 81 | "y": 0.673, 82 | "width": 0.0509, 83 | "path": "tree.svg" 84 | }, 85 | { 86 | "x": 0.614, 87 | "y": 0.3817, 88 | "width": 0.0509, 89 | "path": "tree.svg" 90 | }, 91 | { 92 | "x": 0.8146, 93 | "y": 0.089, 94 | "width": 0.0509, 95 | "path": "tree.svg" 96 | }, 97 | { 98 | "x": 0.9224, 99 | "y": 0.3375, 100 | "width": 0.0232, 101 | "path": "pile_of_logs.svg" 102 | }, 103 | { 104 | "x": 0.6361, 105 | "y": 0.2501, 106 | "campfire": true 107 | }, 108 | { 109 | "x": 0.6995, 110 | "y": 0.1315, 111 | "width": 0.1303, 112 | "path": "tent_medic_mobile.svg", 113 | "action": 4 114 | }, 115 | { 116 | "x": 0.7597, 117 | "y": 0.3242, 118 | "width": 0.1303, 119 | "path": "tent_kiva.svg", 120 | "action": 4 121 | }, 122 | { 123 | "x": 0.9056, 124 | "y": 0.181, 125 | "width": 0.1303, 126 | "path": "tent_flipped.svg" 127 | }, 128 | { 129 | "x": 0.9008, 130 | "y": 0.8291, 131 | "width": 0.1303, 132 | "path": "tent_flipped.svg" 133 | }, 134 | { 135 | "x": 0.6987, 136 | "y": 0.7204, 137 | "width": 0.1303, 138 | "path": "tent_humanitarian_ai.svg", 139 | "action": 4 140 | }, 141 | { 142 | "x": 0.2111, 143 | "y": 0.7797, 144 | "campfire": true 145 | }, 146 | { 147 | "x": 0.2777, 148 | "y": 0.3734, 149 | "width": 0.0428, 150 | "path": "signpost.svg" 151 | }, 152 | { 153 | "x": 0.2263, 154 | "y": 0.2752, 155 | "width": 0.0705, 156 | "path": "picnic_table.svg" 157 | }, 158 | { 159 | "x": 0.6299, 160 | "y": 0.8567, 161 | "width": 0.0702, 162 | "path": "picnic_table_flipped.svg" 163 | }, 164 | { 165 | "x": 0.5922, 166 | "y": 0.1978, 167 | "width": 0.036, 168 | "path": "flashlight_off.svg", 169 | "toggleable": true 170 | }, 171 | { 172 | "x": 0.8313, 173 | "y": 0.7969, 174 | "width": 0.036, 175 | "path": "flashlight_off.svg", 176 | "toggleable": true 177 | }, 178 | { 179 | "x": 0.4988, 180 | "y": 0.12, 181 | "width": 0.0674, 182 | "path": "closed_sign.svg" 183 | }, 184 | { 185 | "x": 0.9491, 186 | "y": 0.5004, 187 | "width": 0.0674, 188 | "path": "closed_sign.svg" 189 | } 190 | ], 191 | "hallways": [ 192 | { 193 | "x": 0.0141, 194 | "y": 0.5042, 195 | "radius": 0.0613, 196 | "to": "home", 197 | "toX": 0.8606, 198 | "toY": 0.8667 199 | }, 200 | { 201 | "x": 0.4687, 202 | "y": 0.9711, 203 | "radius": 0.0662, 204 | "to": "plaza", 205 | "toX": 0.5711, 206 | "toY": 0.0957 207 | } 208 | ] 209 | } 210 | -------------------------------------------------------------------------------- /config/rooms/personal.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "personal_room.svg", 3 | "sponsor": false, 4 | "corners": "0.8032,0.6844;0.4991,0.9980;0.1939,0.6864;0.4991,0.3748", 5 | "elements": [ 6 | { 7 | "x": 0.341, 8 | "y": 0.65, 9 | "width": 0.2514, 10 | "path": "bed.svg" 11 | }, 12 | { 13 | "x": 0.7089, 14 | "y": 0.5792, 15 | "width": 0.1709, 16 | "path": "personal_desk.svg,personal_desk_on.svg", 17 | "toggleable": true, 18 | "state": 0 19 | }, 20 | { 21 | "x": 0.3979, 22 | "y": 0.4486, 23 | "width": 0.0259, 24 | "path": "plant.svg" 25 | }, 26 | { 27 | "x": 0.6821, 28 | "y": 0.6696, 29 | "width": 0.0559, 30 | "path": "chair.svg" 31 | }, 32 | { 33 | "x": 0.5183, 34 | "y": 0.9012, 35 | "width": 0.0907, 36 | "path": "shelf.svg" 37 | }, 38 | { 39 | "x": 0.4674, 40 | "y": 0.3327, 41 | "width": 0.1071, 42 | "path": "closet_closed.svg,closet_open.svg", 43 | "toggleable": true, 44 | "state": 0 45 | } 46 | ], 47 | "hallways": [] 48 | } 49 | -------------------------------------------------------------------------------- /config/rooms/plat.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": ".svg", 3 | "sponsor": true, 4 | "corners": "0.4724,0.3332;0.1889,0.6263;0.5553,0.9972;0.8376,0.7082", 5 | "elements": [ 6 | { 7 | "x": 0.2879, 8 | "y": 0.5083, 9 | "width": 0.0594, 10 | "path": "plat_chair.svg" 11 | }, 12 | { 13 | "x": 0.3788, 14 | "y": 0.6774, 15 | "width": 0.0551, 16 | "path": "plat_chair_back.svg" 17 | }, 18 | { 19 | "x": 0.44, 20 | "y": 0.6215, 21 | "width": 0.0551, 22 | "path": "plat_chair_back.svg" 23 | }, 24 | { 25 | "x": 0.7383, 26 | "y": 0.716, 27 | "width": 0.0319, 28 | "path": "plat_plant_.svg" 29 | }, 30 | { 31 | "x": 0.6571, 32 | "y": 0.837, 33 | "width": 0.1576, 34 | "path": "plat_coffee_table_.svg" 35 | }, 36 | { 37 | "x": 0.66, 38 | "y": 0.7721, 39 | "width": 0.0396, 40 | "path": "plat_coffee_machine.svg" 41 | }, 42 | { 43 | "x": 0.456, 44 | "y": 0.4374, 45 | "width": 0.0898, 46 | "path": "plat_couch.svg" 47 | }, 48 | { 49 | "x": 0.56, 50 | "y": 0.5656, 51 | "width": 0.0941, 52 | "path": "plat_couch_back.svg" 53 | }, 54 | { 55 | "x": 0.504, 56 | "y": 0.5133, 57 | "width": 0.1288, 58 | "path": "plat_rug.svg" 59 | }, 60 | { 61 | "x": 0.5059, 62 | "y": 0.5054, 63 | "width": 0.0599, 64 | "path": "plat_couch_table.svg" 65 | }, 66 | { 67 | "x": 0.565, 68 | "y": 0.4097, 69 | "width": 0.093, 70 | "path": "plat_tv.svg" 71 | }, 72 | { 73 | "x": 0.3211, 74 | "y": 0.5738, 75 | "width": 0.1286, 76 | "path": "plat_desk_.svg" 77 | }, 78 | { 79 | "x": 0.7581, 80 | "y": 0.5396, 81 | "width": 0.0555, 82 | "path": "door.svg" 83 | }, 84 | { 85 | "x": 0.3926, 86 | "y": 0.3393, 87 | "width": 0.0827, 88 | "path": "plat_window.svg" 89 | }, 90 | { 91 | "x": 0.4728, 92 | "y": 0.4217, 93 | "width": 0.0827, 94 | "path": "plat_window.svg" 95 | }, 96 | { 97 | "x": 0.5528, 98 | "y": 0.5034, 99 | "width": 0.0827, 100 | "path": "plat_window.svg" 101 | }, 102 | { 103 | "x": 0.51, 104 | "y": 0.6267, 105 | "width": 0.1655, 106 | "path": "plat_glass_door.svg" 107 | } 108 | ], 109 | "hallways": [ 110 | { 111 | "x": 0.7485, 112 | "y": 0.6185, 113 | "radius": 0.0539, 114 | "to": "plat_area", 115 | "toX": 0.5, 116 | "toY": 0.5 117 | } 118 | ] 119 | } 120 | -------------------------------------------------------------------------------- /config/rooms/plat_area.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "plat_sponsor_area.svg", 3 | "sponsor": false, 4 | "elements": [ 5 | { 6 | "x": 0.6118, 7 | "y": 0.2776, 8 | "width": 0.0412, 9 | "path": "tree2.svg" 10 | }, 11 | { 12 | "x": 0.6778, 13 | "y": 0.1727, 14 | "width": 0.0412, 15 | "path": "tree2.svg" 16 | }, 17 | { 18 | "x": 0.0988, 19 | "y": 0.1272, 20 | "width": 0.08, 21 | "path": "tree.svg" 22 | }, 23 | { 24 | "x": 0.1628, 25 | "y": 0.2679, 26 | "width": 0.08, 27 | "path": "tree.svg" 28 | }, 29 | { 30 | "x": 0.3016, 31 | "y": 0.3976, 32 | "width": 0.08, 33 | "path": "tree.svg" 34 | }, 35 | { 36 | "x": 0.3996, 37 | "y": 0.1877, 38 | "width": 0.08, 39 | "path": "tree.svg" 40 | }, 41 | { 42 | "x": 0.6205, 43 | "y": 0.0494, 44 | "width": 0.08, 45 | "path": "tree.svg" 46 | }, 47 | { 48 | "x": 0.658, 49 | "y": 0.3111, 50 | "width": 0.08, 51 | "path": "tree.svg" 52 | }, 53 | { 54 | "x": 0.8309, 55 | "y": 0.1087, 56 | "width": 0.08, 57 | "path": "tree.svg" 58 | }, 59 | { 60 | "x": 0.9239, 61 | "y": 0.184, 62 | "width": 0.08, 63 | "path": "tree.svg" 64 | }, 65 | { 66 | "x": 0.8517, 67 | "y": 0.5025, 68 | "width": 0.08, 69 | "path": "tree.svg" 70 | }, 71 | { 72 | "x": 0.0953, 73 | "y": 0.2974, 74 | "width": 0.0412, 75 | "path": "tree2.svg" 76 | }, 77 | { 78 | "x": 0.1278, 79 | "y": 0.4949, 80 | "width": 0.0412, 81 | "path": "tree2.svg" 82 | }, 83 | { 84 | "x": 0.0688, 85 | "y": 0.7221, 86 | "width": 0.0412, 87 | "path": "tree2.svg" 88 | }, 89 | { 90 | "x": 0.3133, 91 | "y": 0.106, 92 | "width": 0.0412, 93 | "path": "tree2.svg" 94 | }, 95 | { 96 | "x": 0.3535, 97 | "y": 0.2678, 98 | "width": 0.0412, 99 | "path": "tree2.svg" 100 | }, 101 | { 102 | "x": 0.889, 103 | "y": 0.053, 104 | "width": 0.0412, 105 | "path": "tree2.svg" 106 | }, 107 | { 108 | "x": 0.8765, 109 | "y": 0.3196, 110 | "width": 0.0412, 111 | "path": "tree2.svg" 112 | }, 113 | { 114 | "x": 0.9341, 115 | "y": 0.6258, 116 | "width": 0.0412, 117 | "path": "tree2.svg" 118 | }, 119 | { 120 | "x": 0.0368, 121 | "y": 0.2382, 122 | "width": 0.0509, 123 | "path": "tree2.svg" 124 | }, 125 | { 126 | "x": 0.0471, 127 | "y": 0.5147, 128 | "width": 0.0412, 129 | "path": "tree2.svg" 130 | }, 131 | { 132 | "x": 0.7532, 133 | "y": 0.3583, 134 | "width": 0.1406, 135 | "path": "building_intersystems.svg" 136 | }, 137 | { 138 | "x": 0.2646, 139 | "y": 0.3583, 140 | "width": 0.1496, 141 | "path": "building_cmt.svg" 142 | }, 143 | { 144 | "x": 0.4547, 145 | "y": 0.6668, 146 | "width": 0.0223, 147 | "path": "street_lamp.svg", 148 | "toggleable": true 149 | }, 150 | { 151 | "x": 0.345, 152 | "y": 0.5544, 153 | "width": 0.0223, 154 | "path": "street_lamp.svg", 155 | "toggleable": true 156 | }, 157 | { 158 | "x": 0.3822, 159 | "y": 0.7433, 160 | "width": 0.0223, 161 | "path": "street_lamp.svg", 162 | "toggleable": true 163 | }, 164 | { 165 | "x": 0.6575, 166 | "y": 0.5532, 167 | "width": 0.0223, 168 | "path": "street_lamp.svg", 169 | "toggleable": true 170 | }, 171 | { 172 | "x": 0.5444, 173 | "y": 0.668, 174 | "width": 0.0223, 175 | "path": "street_lamp.svg", 176 | "toggleable": true 177 | }, 178 | { 179 | "x": 0.6795, 180 | "y": 0.7406, 181 | "width": 0.0483, 182 | "path": "bench.svg" 183 | }, 184 | { 185 | "x": 0.3233, 186 | "y": 0.7431, 187 | "width": 0.0483, 188 | "path": "bench_flipped.svg" 189 | }, 190 | { 191 | "x": 0.6181, 192 | "y": 0.7957, 193 | "width": 0.0157, 194 | "path": "recycling_bin.svg" 195 | }, 196 | { 197 | "x": 0.5, 198 | "y": 0.8254, 199 | "width": 0.0879, 200 | "fountain": true 201 | }, 202 | { 203 | "x": 0.3252, 204 | "y": 0.8815, 205 | "width": 0.0704, 206 | "path": "more_sponsors_sign.svg" 207 | }, 208 | { 209 | "x": 0.6748, 210 | "y": 0.8815, 211 | "width": 0.0704, 212 | "path": "more_sponsors_sign_flipped.svg" 213 | } 214 | ], 215 | "hallways": [ 216 | { 217 | "x": 0.5, 218 | "y": 0.03, 219 | "radius": 0.0641, 220 | "to": "home", 221 | "toX": 0.1284, 222 | "toY": 0.8815 223 | }, 224 | { 225 | "x": 0.2946, 226 | "y": 0.6104, 227 | "radius": 0.0483, 228 | "to": "sponsor:cmt", 229 | "toX": 0.7053, 230 | "toY": 0.6853 231 | }, 232 | { 233 | "x": 0.7402, 234 | "y": 0.5717, 235 | "radius": 0.056, 236 | "to": "sponsor:intersystems", 237 | "toX": 0.7053, 238 | "toY": 0.6853 239 | }, 240 | { 241 | "x": 0.3624, 242 | "y": 1.01, 243 | "radius": 0.0739, 244 | "to": "left_field", 245 | "toX": 0.9695, 246 | "toY": 0.402 247 | }, 248 | { 249 | "x": 0.6376, 250 | "y": 1.01, 251 | "radius": 0.0739, 252 | "to": "right_field", 253 | "toX": 0.0305, 254 | "toY": 0.402 255 | } 256 | ] 257 | } 258 | -------------------------------------------------------------------------------- /config/rooms/plaza.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "plaza.svg", 3 | "elements": [ 4 | { 5 | "x": 0.0339, 6 | "y": 0.4501, 7 | "width": 0.0223, 8 | "path": "street_lamp.svg", 9 | "toggleable": true 10 | }, 11 | { 12 | "x": 0.1737, 13 | "y": 0.4672, 14 | "width": 0.0223, 15 | "path": "street_lamp.svg", 16 | "toggleable": true 17 | }, 18 | { 19 | "x": 0.0911, 20 | "y": 0.3212, 21 | "width": 0.0223, 22 | "path": "street_lamp.svg", 23 | "toggleable": true 24 | }, 25 | { 26 | "x": 0.1564, 27 | "y": 0.208, 28 | "width": 0.0223, 29 | "path": "street_lamp.svg", 30 | "toggleable": true 31 | }, 32 | { 33 | "x": 0.2427, 34 | "y": 0.1335, 35 | "width": 0.0223, 36 | "path": "street_lamp.svg", 37 | "toggleable": true 38 | }, 39 | { 40 | "x": 0.3266, 41 | "y": 0.0733, 42 | "width": 0.0223, 43 | "path": "street_lamp.svg", 44 | "toggleable": true 45 | }, 46 | { 47 | "x": 0.404, 48 | "y": -0.0112, 49 | "width": 0.0223, 50 | "path": "street_lamp.svg", 51 | "toggleable": true 52 | }, 53 | { 54 | "x": 0.1737, 55 | "y": 0.4672, 56 | "width": 0.2015, 57 | "path": "coffee_shop.svg" 58 | }, 59 | { 60 | "x": 0.887, 61 | "y": 0.1314, 62 | "width": 0.3928, 63 | "path": "stadium.svg", 64 | "changingImagePath": true, 65 | "changingPaths": "stadium.svg,stadium2.svg", 66 | "changingInterval": 1000, 67 | "changingRandomly": false 68 | }, 69 | { 70 | "x": 0.0561, 71 | "y": 0.6435, 72 | "width": 0.0523, 73 | "path": "outdoor_dining_table.svg" 74 | }, 75 | { 76 | "x": 0.5005, 77 | "y": 0.8824, 78 | "width": 0.0536, 79 | "path": "tree.svg" 80 | }, 81 | { 82 | "x": 0.7915, 83 | "y": 0.3392, 84 | "width": 0.0536, 85 | "path": "tree.svg" 86 | }, 87 | { 88 | "x": 0.977, 89 | "y": 0.3342, 90 | "width": 0.0536, 91 | "path": "tree.svg" 92 | }, 93 | { 94 | "x": 0.5005, 95 | "y": 0.8824, 96 | "width": 0.0536, 97 | "path": "tree.svg" 98 | }, 99 | { 100 | "x": 0.2431, 101 | "y": 0.2651, 102 | "width": 0.0536, 103 | "path": "tree.svg" 104 | }, 105 | { 106 | "x": 0.3327, 107 | "y": 0.1577, 108 | "width": 0.0536, 109 | "path": "tree.svg" 110 | }, 111 | { 112 | "x": 0.9436, 113 | "y": 0.7666, 114 | "width": 0.0536, 115 | "path": "tree.svg" 116 | }, 117 | { 118 | "x": 0.8931, 119 | "y": 0.8836, 120 | "width": 0.0327, 121 | "path": "tree2.svg" 122 | }, 123 | { 124 | "x": 0.0282, 125 | "y": 0.7768, 126 | "width": 0.0327, 127 | "path": "tree2.svg" 128 | }, 129 | { 130 | "x": 0.0838, 131 | "y": 0.7151, 132 | "width": 0.0327, 133 | "path": "tree2.svg" 134 | }, 135 | { 136 | "x": 0.2567, 137 | "y": 0.7867, 138 | "width": 0.0327, 139 | "path": "tree2.svg" 140 | }, 141 | { 142 | "x": 0.1026, 143 | "y": 0.9398, 144 | "width": 0.0327, 145 | "path": "tree2.svg" 146 | }, 147 | { 148 | "x": 0.1561, 149 | "y": 0.8867, 150 | "width": 0.0327, 151 | "path": "tree2.svg" 152 | }, 153 | { 154 | "x": 0.2067, 155 | "y": 0.8361, 156 | "width": 0.0327, 157 | "path": "tree2.svg" 158 | }, 159 | { 160 | "x": 0.4949, 161 | "y": 0.1817, 162 | "width": 0.0327, 163 | "path": "tree2.svg" 164 | }, 165 | { 166 | "x": 0.4199, 167 | "y": 0.1126, 168 | "width": 0.0327, 169 | "path": "tree2.svg" 170 | }, 171 | { 172 | "x": 0.4734, 173 | "y": 0.0657, 174 | "width": 0.0327, 175 | "path": "tree2.svg" 176 | }, 177 | { 178 | "x": 0.5282, 179 | "y": 0.0113, 180 | "width": 0.0327, 181 | "path": "tree2.svg" 182 | }, 183 | { 184 | "x": 0.5811, 185 | "y": -0.0442, 186 | "width": 0.0327, 187 | "path": "tree2.svg" 188 | }, 189 | { 190 | "x": 0.754, 191 | "y": 0.2743, 192 | "width": 0.0327, 193 | "path": "tree2.svg" 194 | }, 195 | { 196 | "x": 0.547, 197 | "y": 0.1311, 198 | "width": 0.0327, 199 | "path": "tree2.svg" 200 | }, 201 | { 202 | "x": 0.6012, 203 | "y": 0.0768, 204 | "width": 0.0327, 205 | "path": "tree2.svg" 206 | }, 207 | { 208 | "x": 0.6589, 209 | "y": 0.02, 210 | "width": 0.0327, 211 | "path": "tree2.svg" 212 | }, 213 | { 214 | "x": 0.1387, 215 | "y": 0.662, 216 | "width": 0.0327, 217 | "path": "tree2.svg" 218 | }, 219 | { 220 | "x": 0.6234, 221 | "y": 0.4228, 222 | "width": 0.3401, 223 | "path": "arcade.svg", 224 | "action": 7 225 | }, 226 | { 227 | "x": 0.3615, 228 | "y": 0.5731, 229 | "width": 0.0436, 230 | "path": "bench.svg" 231 | }, 232 | { 233 | "x": 0.4264, 234 | "y": 0.6472, 235 | "width": 0.0906, 236 | "fountain": true 237 | }, 238 | { 239 | "x": 0.4885, 240 | "y": 0.7571, 241 | "width": 0.0436, 242 | "path": "bench.svg" 243 | }, 244 | { 245 | "x": 0.3557, 246 | "y": 0.7546, 247 | "width": 0.0436, 248 | "path": "bench_flipped.svg" 249 | }, 250 | { 251 | "x": 0.4981, 252 | "y": 0.5806, 253 | "width": 0.0436, 254 | "path": "bench_flipped.svg" 255 | }, 256 | { 257 | "x": 0.0579, 258 | "y": 0.8025, 259 | "width": 0.0264, 260 | "path": "bench_smol.svg" 261 | }, 262 | { 263 | "x": 0.2323, 264 | "y": 0.8716, 265 | "width": 0.0264, 266 | "path": "bench_smol.svg" 267 | }, 268 | { 269 | "x": 0.4577, 270 | "y": 0.4198, 271 | "width": 0.0353, 272 | "path": "tree_smol.svg" 273 | }, 274 | { 275 | "x": 0.6819, 276 | "y": 0.6495, 277 | "width": 0.0353, 278 | "path": "tree_smol.svg" 279 | } 280 | ], 281 | "hallways": [ 282 | { 283 | "x": 0.2034, 284 | "y": 0.5945, 285 | "radius": 0.0429, 286 | "to": "coffee_shop", 287 | "toX": 0.6637, 288 | "toY": 0.283 289 | }, 290 | { 291 | "x": 0.0544, 292 | "y": 0.9247, 293 | "radius": 0.0885, 294 | "to": "right_field", 295 | "toX": 0.9772, 296 | "toY": 0.41 297 | }, 298 | { 299 | "x": 0.6241, 300 | "y": 0.0286, 301 | "radius": 0.0654, 302 | "to": "home", 303 | "toX": 0.8606, 304 | "toY": 0.8667 305 | }, 306 | { 307 | "x": 0.8882, 308 | "y": 0.3832, 309 | "radius": 0.0949, 310 | "to": "auditorium", 311 | "toX": 0.8126, 312 | "toY": 0.5626 313 | } 314 | ] 315 | } 316 | -------------------------------------------------------------------------------- /config/rooms/right_field.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": "right_field.svg", 3 | "sponsor": false, 4 | "elements": [ 5 | { 6 | "x": 0.8679, 7 | "y": 0.104, 8 | "width": 0.0558, 9 | "path": "tree.svg" 10 | }, 11 | { 12 | "x": 0.7648, 13 | "y": 0.0495, 14 | "width": 0.0558, 15 | "path": "tree.svg" 16 | }, 17 | { 18 | "x": 0.6522, 19 | "y": 0.1756, 20 | "width": 0.0558, 21 | "path": "tree.svg" 22 | }, 23 | { 24 | "x": 0.4992, 25 | "y": 0.1412, 26 | "width": 0.0558, 27 | "path": "tree.svg" 28 | }, 29 | { 30 | "x": 0.0367, 31 | "y": 0.0782, 32 | "width": 0.0558, 33 | "path": "tree.svg" 34 | }, 35 | { 36 | "x": 0.5823, 37 | "y": 0.3217, 38 | "width": 0.04, 39 | "path": "tree2.svg" 40 | }, 41 | { 42 | "x": 0.9417, 43 | "y": 0.1211, 44 | "width": 0.04, 45 | "path": "tree2.svg" 46 | }, 47 | { 48 | "x": 0.7942, 49 | "y": 0.2128, 50 | "width": 0.04, 51 | "path": "tree2.svg" 52 | }, 53 | { 54 | "x": 0.3422, 55 | "y": 0.104, 56 | "width": 0.04, 57 | "path": "tree2.svg" 58 | }, 59 | { 60 | "x": 0.1934, 61 | "y": 0.31, 62 | "width": 0.22, 63 | "path": "building_leah.svg" 64 | }, 65 | { 66 | "x": 0.376, 67 | "y": 0.54, 68 | "width": 0.22, 69 | "path": "building_lsa.svg" 70 | }, 71 | { 72 | "x": 0.84, 73 | "y": 0.275, 74 | "width": 0.22, 75 | "path": "building_lincoln.svg" 76 | }, 77 | { 78 | "x": 0.63, 79 | "y": 0.48, 80 | "width": 0.23, 81 | "path": "building_medscience.svg" 82 | }, 83 | { 84 | "x": 0.0575, 85 | "y": 0.7759, 86 | "width": 0.0558, 87 | "path": "tree.svg" 88 | }, 89 | { 90 | "x": 0.9527, 91 | "y": 0.7816, 92 | "width": 0.0558, 93 | "path": "tree.svg" 94 | }, 95 | { 96 | "x": 0.8108, 97 | "y": 0.879, 98 | "width": 0.0558, 99 | "path": "tree.svg" 100 | }, 101 | { 102 | "x": 0.1202, 103 | "y": 0.8645, 104 | "width": 0.04, 105 | "path": "tree2.svg" 106 | }, 107 | { 108 | "x": 0.5077, 109 | "y": 0.6381, 110 | "width": 0.0287, 111 | "path": "tree2.svg" 112 | }, 113 | { 114 | "x": 0.5, 115 | "y": 0.8232, 116 | "width": 0.0906, 117 | "fountain": true 118 | }, 119 | { 120 | "x": 0.4337, 121 | "y": 0.9235, 122 | "width": 0.0436, 123 | "path": "bench_flipped.svg" 124 | }, 125 | { 126 | "x": 0.5602, 127 | "y": 0.9321, 128 | "width": 0.0436, 129 | "path": "bench.svg" 130 | }, 131 | { 132 | "x": 0.0227, 133 | "y": 0.4392, 134 | "width": 0.0223, 135 | "path": "street_lamp.svg", 136 | "toggleable": true 137 | }, 138 | { 139 | "x": 0.1169, 140 | "y": 0.5324, 141 | "width": 0.0223, 142 | "path": "street_lamp.svg", 143 | "toggleable": true 144 | }, 145 | { 146 | "x": 0.0227, 147 | "y": 0.4392, 148 | "width": 0.0223, 149 | "path": "street_lamp.svg", 150 | "toggleable": true 151 | }, 152 | { 153 | "x": 0.2087, 154 | "y": 0.6212, 155 | "width": 0.0223, 156 | "path": "street_lamp.svg", 157 | "toggleable": true 158 | }, 159 | { 160 | "x": 0.33520000000000005, 161 | "y": 0.753, 162 | "width": 0.0223, 163 | "path": "street_lamp.svg", 164 | "toggleable": true 165 | }, 166 | { 167 | "x": 0.6583, 168 | "y": 0.7659, 169 | "width": 0.0223, 170 | "path": "street_lamp.svg", 171 | "toggleable": true 172 | }, 173 | { 174 | "x": 0.7549, 175 | "y": 0.667, 176 | "width": 0.0223, 177 | "path": "street_lamp.svg", 178 | "toggleable": true 179 | }, 180 | { 181 | "x": 0.8685, 182 | "y": 0.5481, 183 | "width": 0.0223, 184 | "path": "street_lamp.svg", 185 | "toggleable": true 186 | }, 187 | { 188 | "x": 0.5, 189 | "y": 0.9488, 190 | "width": 0.0189, 191 | "path": "recycling_bin.svg" 192 | }, 193 | { 194 | "x": 0.237, 195 | "y": 0.5375, 196 | "width": 0.0213, 197 | "path": "bike_rack.svg" 198 | } 199 | ], 200 | "hallways": [ 201 | { 202 | "x": 0.0082, 203 | "y": 0.3787, 204 | "radius": 0.0579, 205 | "to": "left_field", 206 | "toX": 0.6113, 207 | "toY": 0.9628 208 | }, 209 | { 210 | "x": 0.1371, 211 | "y": 0.4574, 212 | "radius": 0.0525, 213 | "to": "sponsor:leah", 214 | "toX": 0.4961, 215 | "toY": 0.5002 216 | }, 217 | { 218 | "x": 0.3171, 219 | "y": 0.6522, 220 | "radius": 0.052, 221 | "to": "sponsor:lsa", 222 | "toX": 0.4961, 223 | "toY": 0.5002 224 | }, 225 | { 226 | "x": 0.6971, 227 | "y": 0.6202, 228 | "radius": 0.0509, 229 | "to": "sponsor:medscience", 230 | "toX": 0.4961, 231 | "toY": 0.5002 232 | }, 233 | { 234 | "x": 0.8933, 235 | "y": 0.4077, 236 | "radius": 0.0479, 237 | "to": "sponsor:lincoln", 238 | "toX": 0.4961, 239 | "toY": 0.5002 240 | }, 241 | { 242 | "x": 0.9977, 243 | "y": 0.3837, 244 | "radius": 0.0521, 245 | "to": "plaza", 246 | "toX": 0.0445, 247 | "toY": 0.9362 248 | } 249 | ] 250 | } 251 | -------------------------------------------------------------------------------- /config/rooms/silver.json: -------------------------------------------------------------------------------- 1 | { 2 | "background": ".svg", 3 | "sponsor": true, 4 | "corners": "0.8260,0.6037;0.4424,0.9992;0.1728,0.7246;0.5541,0.3251", 5 | "elements": [ 6 | { 7 | "x": 0.5082, 8 | "y": 0.2957, 9 | "width": 0.0555, 10 | "path": "door_flipped.svg" 11 | }, 12 | { 13 | "x": 0.652, 14 | "y": 0.4814, 15 | "width": 0.219, 16 | "path": "desk_.svg" 17 | }, 18 | { 19 | "x": 0.5654, 20 | "y": 0.7142, 21 | "width": 0.0368, 22 | "path": "plant_.svg" 23 | }, 24 | { 25 | "x": 0.4164, 26 | "y": 0.4608, 27 | "width": 0.1159, 28 | "path": "shelf_yellow.svg" 29 | }, 30 | { 31 | "x": 0.5598, 32 | "y": 0.297, 33 | "width": 0.0319, 34 | "path": "plant_brown.svg" 35 | }, 36 | { 37 | "x": 0.3092, 38 | "y": 0.6434, 39 | "width": 0.1577, 40 | "path": "sofa_front.svg" 41 | }, 42 | { 43 | "x": 0.3858, 44 | "y": 0.7356, 45 | "width": 0.1112, 46 | "path": "coffee_table.svg" 47 | }, 48 | { 49 | "x": 0.4688, 50 | "y": 0.8247, 51 | "width": 0.1744, 52 | "path": "sofa_back.svg" 53 | } 54 | ], 55 | "hallways": [ 56 | { 57 | "x": 0.5107, 58 | "y": 0.3836, 59 | "radius": 0.0572, 60 | "to": "left_field", 61 | "toX": 0.5, 62 | "toY": 0.5 63 | } 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /config/sponsors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "beaverworks", 4 | "name": "MIT Beaver Works" 5 | }, 6 | { 7 | "id": "HMS MEDscience", 8 | "name": "medscience" 9 | }, 10 | { 11 | "id": "ieee", 12 | "name": "IEEE" 13 | }, 14 | { 15 | "id": "kodewithklossy", 16 | "name": "Kode with Klossy" 17 | }, 18 | { 19 | "id": "ktbyte", 20 | "name": "KTBYTE" 21 | }, 22 | { 23 | "id": "lsa", 24 | "name": "Latino Stem Alliance" 25 | }, 26 | { 27 | "id": "leah", 28 | "name": "LEAH Project" 29 | }, 30 | { 31 | "id": "lincoln", 32 | "name": "MIT Lincoln Lab" 33 | } 34 | ] 35 | -------------------------------------------------------------------------------- /controllers/room.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/techx/playground/db" 7 | "github.com/techx/playground/db/models" 8 | "github.com/techx/playground/utils" 9 | 10 | "github.com/go-redis/redis/v7" 11 | "github.com/labstack/echo/v4" 12 | ) 13 | 14 | type RoomController struct{} 15 | 16 | // GET /rooms - get all rooms 17 | func (r RoomController) GetRooms(c echo.Context) error { 18 | // Get all of the room names from Redis 19 | roomNames, err := db.GetInstance().SMembers("rooms").Result() 20 | 21 | if err != nil { 22 | return echo.NewHTTPError(http.StatusInternalServerError, 23 | "database error") 24 | } 25 | 26 | // Load each room into this array 27 | pip := db.GetInstance().Pipeline() 28 | roomCmds := make([]*redis.StringStringMapCmd, len(roomNames)) 29 | 30 | for i, name := range roomNames { 31 | roomCmds[i] = pip.HGetAll("room:" + name) 32 | } 33 | 34 | pip.Exec() 35 | rooms := make([]models.Room, len(roomNames)) 36 | 37 | for i, roomCmd := range roomCmds { 38 | roomData, _ := roomCmd.Result() 39 | utils.Bind(roomData, &rooms[i]) 40 | } 41 | 42 | return c.JSON(http.StatusOK, rooms) 43 | } 44 | -------------------------------------------------------------------------------- /database.md: -------------------------------------------------------------------------------- 1 | # Database Schema 2 | 3 | - `character:` (hash) 4 | - `character::teammates` (set) 5 | - `character::friends` (set) 6 | - `character::requests` (set) 7 | - List of IDs of people who have added this person as a friend, but this character has not added back yet 8 | - `conversation::` (list) 9 | - Ordering of character IDs comes from a hash of each ID -- check `socket/hub.go` for more details 10 | - List of message IDs (in chronological order) 11 | - `element:` (hash) 12 | - `hallway:` (hash) 13 | - `locations` (set) 14 | - `location:` (hash) 15 | - `message:` (hash) 16 | - `emailToCharacter` (hash) 17 | - Mapping of email addresses to Playground character IDs 18 | - Used for all non-hackers (sponsors, mentors, organizers) 19 | - `emailToSponsor` (hash) 20 | - Mapping of email addresses to sponsor IDs, if the email corresponds to a company rep 21 | - `quillToCharacter` (hash) 22 | - Mapping of Quill user IDs to Playground character IDs 23 | - `room:` (hash) 24 | - `room::elements` (list) 25 | - Ordering of elements indicates layering -- right-most indicates top in layer-wise order 26 | - `room::hallways` (set) 27 | - `room::characters` (set) 28 | - `rooms` (set) 29 | - `song:` (hash) 30 | - `songs` (list) 31 | - `sponsors` (set) 32 | - `sponsor:` (hash) 33 | - `sponsor::subscribed` (set) 34 | - `sponsor::hackerqueue` (list) 35 | - `event:` (string) 36 | - `event::attendees` (set) 37 | - `character::events` (set) 38 | - `events` (set) 39 | -------------------------------------------------------------------------------- /db/connector.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/rand" 7 | "os" 8 | "strconv" 9 | "strings" 10 | "time" 11 | 12 | "github.com/google/uuid" 13 | "github.com/techx/playground/config" 14 | "github.com/techx/playground/db/models" 15 | "github.com/techx/playground/utils" 16 | 17 | mapset "github.com/deckarep/golang-set" 18 | "github.com/go-redis/redis/v7" 19 | ) 20 | 21 | const ingestClientName string = "ingest" 22 | 23 | var ( 24 | ingestID string 25 | instance *redis.Client 26 | leader bool 27 | psc *redis.PubSub 28 | ) 29 | 30 | // Init creates the database connection 31 | func Init(shouldReset bool) { 32 | config := config.GetConfig() 33 | 34 | dbAddr := os.Getenv("DATABASE_ADDR") 35 | dbPass := os.Getenv("DATABASE_PASS") 36 | 37 | if dbAddr == "" { 38 | instance = redis.NewClient(&redis.Options{ 39 | Addr: config.GetString("db.addr"), 40 | Password: config.GetString("db.password"), 41 | DB: config.GetInt("db.db"), 42 | }) 43 | } else { 44 | instance = redis.NewClient(&redis.Options{ 45 | Addr: dbAddr, 46 | Password: dbPass, 47 | DB: 0, 48 | }) 49 | } 50 | 51 | if shouldReset { 52 | reset() 53 | } 54 | 55 | // Save our ingest ID 56 | ingestID = uuid.New().String() 57 | 58 | // Initialize jukebox 59 | // TODO: Make sure this works correctly when there are multiple ingests 60 | initialTime := time.Now().Unix() 61 | instance.Set("queuestatus", initialTime, 0) 62 | instance.Set("currentsong", 0, 0) 63 | 64 | // Update TIM the beaver 65 | character := *models.NewTIMCharacter() 66 | instance.HSet("character:tim", utils.StructToMap(character)) 67 | instance.SAdd("room:home:characters", "tim") 68 | } 69 | 70 | func GetIngestID() string { 71 | return ingestID 72 | } 73 | 74 | func GetInstance() *redis.Client { 75 | return instance 76 | } 77 | 78 | func ListenForUpdates(callback func(msg []byte)) { 79 | ingests, err := instance.LRange("ingests", 0, -1).Result() 80 | 81 | if err != nil { 82 | panic(err) 83 | } 84 | 85 | // Let other ingest servers know about this one 86 | instance.Publish("ingest", ingestID) 87 | 88 | // subscribe to existing ingests, send id to master 89 | psc = instance.Subscribe(append(ingests, "ingest", "all")...) 90 | 91 | // Add this ingest to the ingests set 92 | instance.RPush("ingests", ingestID) 93 | 94 | for { 95 | msg, err := psc.ReceiveMessage() 96 | 97 | if err != nil { 98 | // Stop server if we disconnect from Redis 99 | panic(err) 100 | } 101 | 102 | if msg.Channel == "ingest" { 103 | // If this is a new ingest server, subscribe to it 104 | psc.Subscribe(msg.Payload) 105 | } else { 106 | callback([]byte(msg.Payload)) 107 | } 108 | } 109 | } 110 | 111 | func MonitorLeader() { 112 | i := 0 113 | 114 | for range time.NewTicker(time.Second).C { 115 | // Set our name so we can identify this client as an ingest 116 | // Not sure why, but the client names occasionally get reset -- let's do it every second 117 | pip := instance.Pipeline() 118 | pip.Process(redis.NewStringCmd("client", "setname", ingestID)) 119 | clientsCmd := instance.ClientList() 120 | pip.Exec() 121 | 122 | clientsRes, _ := clientsCmd.Result() 123 | clients := strings.Split(clientsRes, "\n") 124 | 125 | connectedIngestIDs := mapset.NewSet() 126 | 127 | for _, client := range clients { 128 | clientParts := strings.Split(client, " ") 129 | 130 | if len(clientParts) < 4 { 131 | // Probably a newline, invalid client 132 | continue 133 | } 134 | 135 | clientName := strings.Split(clientParts[3], "=")[1] 136 | 137 | if len(clientName) != 36 { 138 | // This isn't a uuid, not an ingest 139 | continue 140 | } 141 | 142 | connectedIngestIDs.Add(clientName) 143 | } 144 | 145 | var leaderID string 146 | ingestIDs, _ := instance.LRange("ingests", 0, -1).Result() 147 | 148 | for _, ingestID := range ingestIDs { 149 | if connectedIngestIDs.Contains(ingestID) { 150 | leaderID = ingestID 151 | break 152 | } 153 | } 154 | 155 | // If we're not the leader, don't do any leader actions 156 | if leaderID != ingestID { 157 | continue 158 | } 159 | 160 | ////////////////////////////////////////////// 161 | // ALL CODE BELOW IS ONLY RUN ON THE LEADER // 162 | ////////////////////////////////////////////// 163 | 164 | // Mark ourselves as the leader 165 | leader = true 166 | 167 | // Take care of ingest servers that got disconnected 168 | for _, ingestID := range ingestIDs { 169 | if connectedIngestIDs.Contains(ingestID) { 170 | // This ingest is currently connected to Redis 171 | continue 172 | } 173 | 174 | fmt.Println("removing", ingestID) 175 | 176 | // Remove characters connected to this ingest from their rooms 177 | characters, _ := instance.SMembers("ingest:" + ingestID + ":characters").Result() 178 | 179 | pip := instance.Pipeline() 180 | roomCmds := make([]*redis.StringCmd, len(characters)) 181 | 182 | for i, characterID := range characters { 183 | roomCmds[i] = pip.HGet("character:"+characterID, "room") 184 | } 185 | 186 | pip.Exec() 187 | pip = instance.Pipeline() 188 | 189 | for i, roomCmd := range roomCmds { 190 | room, _ := roomCmd.Result() 191 | pip.SRem("room:"+room, characters[i]) 192 | } 193 | 194 | pip.Exec() 195 | 196 | // Ingest has been taken care of, remove from set 197 | instance.LRem("ingests", 0, ingestID) 198 | } 199 | 200 | // Get song queue status 201 | queueRes, _ := instance.Get("queuestatus").Result() 202 | queueStatus, _ := strconv.ParseInt(queueRes, 10, 64) 203 | 204 | songEnd := time.Unix(queueStatus, 0) 205 | 206 | // If current song ended, start next song (if there is one) 207 | if songEnd.Before(time.Now()) { 208 | queueLength, _ := instance.LLen("songs").Result() 209 | 210 | if queueLength > 0 { 211 | // Pop the next song off the queue 212 | songID, _ := instance.LPop("songs").Result() 213 | instance.Set("currentsong", songID, 0) 214 | 215 | songRes, _ := instance.HGetAll("song:" + songID).Result() 216 | var song models.Song 217 | utils.Bind(songRes, &song) 218 | 219 | // Update queue status to reflect new song 220 | endTime := (time.Now().Add(time.Second * time.Duration(song.Duration))).Unix() 221 | instance.Set("queuestatus", endTime, 0) 222 | 223 | song.ID = songID 224 | // Send song packet to ingests 225 | playSongPacket := map[string]interface{}{ 226 | "type": "play_song", 227 | "song": song, 228 | "start": 0, 229 | "end": int(song.Duration), 230 | } 231 | 232 | data, _ := json.Marshal(playSongPacket) 233 | pip.Publish("all", data) 234 | pip.Exec() 235 | } 236 | } 237 | 238 | if i%15 == 0 { 239 | characterRes, _ := instance.HGetAll("character:tim").Result() 240 | var tim models.Character 241 | tim.ID = "tim" 242 | utils.Bind(characterRes, &tim) 243 | 244 | whatToDo := rand.Float64() 245 | 246 | walkProb := config.GetConfig().GetFloat64("tim.action_probs.walk") 247 | talkProb := config.GetConfig().GetFloat64("tim.action_probs.talk") 248 | teleportProb := config.GetConfig().GetFloat64("tim.action_probs.teleport") 249 | 250 | if whatToDo < teleportProb { 251 | hallwaysRes, _ := instance.SMembers("room:" + tim.Room + ":hallways").Result() 252 | 253 | if len(hallwaysRes) == 0 { 254 | continue 255 | } 256 | 257 | // Make sure tim can only walk into rooms without walls 258 | hallwayCmds := make([]*redis.StringCmd, len(hallwaysRes)) 259 | 260 | pip := instance.Pipeline() 261 | 262 | for j, hallwayID := range hallwaysRes { 263 | hallwayCmds[j] = pip.HGet("hallway:"+hallwayID, "to") 264 | } 265 | 266 | pip.Exec() 267 | 268 | hallwayOptions := make([]string, 0) 269 | 270 | for j, cmd := range hallwayCmds { 271 | roomID, _ := cmd.Result() 272 | 273 | for _, allowedRoomID := range config.GetConfig().GetStringSlice("tim.allowed_rooms") { 274 | if roomID == allowedRoomID { 275 | hallwayOptions = append(hallwayOptions, hallwaysRes[j]) 276 | break 277 | } 278 | } 279 | } 280 | 281 | // Teleport into the allowed room 282 | hallwayID := hallwayOptions[rand.Intn(len(hallwayOptions))] 283 | 284 | hallwayRes, _ := instance.HGetAll("hallway:" + hallwayID).Result() 285 | var hallway models.Hallway 286 | utils.Bind(hallwayRes, &hallway) 287 | 288 | movePacket := map[string]interface{}{ 289 | "type": "move", 290 | "id": "tim", 291 | "room": tim.Room, 292 | "x": hallway.X, 293 | "y": hallway.Y, 294 | } 295 | data, _ := json.Marshal(movePacket) 296 | 297 | pip = instance.Pipeline() 298 | pip.HSet("character:tim", "x", hallway.X) 299 | pip.HSet("character:tim", "y", hallway.Y) 300 | pip.Publish("all", data) 301 | pip.Exec() 302 | 303 | time.AfterFunc(4*time.Second, func() { 304 | pip := instance.Pipeline() 305 | pip.SRem("room:"+tim.Room+":characters", "tim") 306 | pip.SAdd("room:"+hallway.To+":characters", "tim") 307 | pip.HSet("character:tim", "room", hallway.To) 308 | pip.HSet("character:tim", "x", hallway.ToX) 309 | pip.HSet("character:tim", "y", hallway.ToY) 310 | 311 | timData, _ := tim.MarshalBinary() 312 | var newTimData map[string]interface{} 313 | json.Unmarshal(timData, &newTimData) 314 | 315 | teleportPacket := map[string]interface{}{ 316 | "type": "teleport", 317 | "character": newTimData, 318 | "from": tim.Room, 319 | "to": hallway.To, 320 | "x": hallway.ToX, 321 | "y": hallway.ToY, 322 | } 323 | 324 | data, _ = json.Marshal(teleportPacket) 325 | pip.Publish("all", data) 326 | pip.Exec() 327 | }) 328 | } else if whatToDo < teleportProb+walkProb { 329 | x := rand.Float64() 330 | y := rand.Float64() 331 | 332 | movePacket := map[string]interface{}{ 333 | "type": "move", 334 | "id": "tim", 335 | "room": tim.Room, 336 | "x": x, 337 | "y": y, 338 | } 339 | data, _ := json.Marshal(movePacket) 340 | 341 | pip := instance.Pipeline() 342 | pip.HSet("character:tim", "x", x) 343 | pip.HSet("character:tim", "y", y) 344 | pip.Publish("all", data) 345 | pip.Exec() 346 | } else if whatToDo < teleportProb+walkProb+talkProb { 347 | timLines := config.GetConfig().GetStringSlice("tim.chat_lines") 348 | randomLine := timLines[rand.Intn(len(timLines))] 349 | 350 | chatPacket := map[string]interface{}{ 351 | "type": "chat", 352 | "id": "tim", 353 | "mssg": randomLine, 354 | "room": tim.Room, 355 | } 356 | 357 | data, _ := json.Marshal(chatPacket) 358 | instance.Publish("all", data) 359 | } 360 | } 361 | 362 | i++ 363 | } 364 | } 365 | 366 | func Publish(msg interface{}) { 367 | instance.Publish(ingestID, msg) 368 | } 369 | -------------------------------------------------------------------------------- /db/doc.go: -------------------------------------------------------------------------------- 1 | // Package db provides functions for reading from and writing to our Redis-based database 2 | package db 3 | -------------------------------------------------------------------------------- /db/models/achievements.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | // Character is the digital representation of a client 4 | type Achievements struct { 5 | CompanyTour bool `json:"companyTour" redis:"companyTour"` 6 | PeerExpo bool `json:"peerExpo" redis:"peerExpo"` 7 | Hangouts bool `json:"hangouts" redis:"hangouts"` 8 | Workshops bool `json:"workshops" redis:"workshops"` 9 | SocialMedia bool `json:"socialMedia" redis:"socialMedia"` 10 | MemeLord bool `json:"memeLord" redis:"memeLord"` 11 | SponsorQueue bool `json:"sponsorQueue" redis:"sponsorQueue"` 12 | SendLocation bool `json:"sendLocation" redis:"sendLocation"` 13 | TrackCounter bool `json:"trackCounter" redis:"trackCounter"` 14 | MiniEvents bool `json:"miniEvents" redis:"miniEvents"` 15 | } 16 | -------------------------------------------------------------------------------- /db/models/character.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | 7 | "github.com/google/uuid" 8 | "github.com/techx/playground/config" 9 | ) 10 | 11 | type Role int 12 | 13 | const ( 14 | Guest Role = iota 15 | Organizer 16 | SponsorRep 17 | Mentor 18 | Hacker 19 | 20 | defaultEyeColor = "#634e34" 21 | defaultSkinColor = "#e0ac69" 22 | defaultShirtColor = "#d6e2f8" 23 | defaultPantsColor = "#ecf0f1" 24 | ) 25 | 26 | // Character is the digital representation of a client 27 | type Character struct { 28 | ID string `json:"id" redis:"-"` 29 | Name string `json:"name" redis:"name"` 30 | Location string `json:"location" redis:"location"` 31 | Bio string `json:"bio" redis:"bio"` 32 | School string `json:"school" redis:"school"` 33 | GradYear int `json:"gradYear" redis:"gradYear"` 34 | X float64 `json:"x" redis:"x"` 35 | Y float64 `json:"y" redis:"y"` 36 | Room string `json:"room" redis:"room"` 37 | Ingest string `json:"ingest" redis:"ingest"` 38 | FeedbackOpened bool `json:"feedbackOpened" redis:"feedbackOpened"` 39 | Email string `json:"email" redis:"email"` 40 | Role int `json:"role" redis:"role"` 41 | IsCollege bool `json:"isCollege" redis:"isCollege"` 42 | 43 | // If this character is in a queue, this is the sponsor ID of the queue they're in 44 | QueueID string `json:"queueId" redis:"queueId"` 45 | 46 | // If this character is a sponsor rep, this is their company's ID and personal zoom link 47 | SponsorID string `json:"sponsorId,omitempty" redis:"sponsorId"` 48 | Zoom string `json:"zoom,omitempty" redis:"zoom"` 49 | 50 | // This character's project, if they have one 51 | Project *Project `json:"project" redis:"-"` 52 | 53 | // Used to track progress on certain achievements 54 | NumSponsorsVisited int `json:"numSponsorsVisited" redis:"numSponsorsVisited"` 55 | VisitedNonprofits bool `json:"visitedNonprofits" redis:"visitedNonprofits"` 56 | NumWorkshops int `json:"numWorkshops" redis:"numWorkshops"` 57 | NumMiniEvents int `json:"numMiniEvents" redis:"numMiniEvents"` 58 | 59 | // Clothes 60 | EyeColor string `json:"eyeColor" redis:"eyeColor"` 61 | SkinColor string `json:"skinColor" redis:"skinColor"` 62 | ShirtColor string `json:"shirtColor" redis:"shirtColor"` 63 | PantsColor string `json:"pantsColor" redis:"pantsColor"` 64 | } 65 | 66 | func NewCharacter(name string) *Character { 67 | c := new(Character) 68 | c.ID = uuid.New().String() 69 | c.Name = name 70 | c.GradYear = 2022 71 | c.X = 0.5 72 | c.Y = 0.5 73 | c.Room = "home" 74 | c.Role = int(Organizer) 75 | c.EyeColor = defaultEyeColor 76 | c.SkinColor = defaultSkinColor 77 | c.ShirtColor = defaultShirtColor 78 | c.PantsColor = defaultPantsColor 79 | return c 80 | } 81 | 82 | func NewTIMCharacter() *Character { 83 | c := new(Character) 84 | c.ID = "tim" 85 | c.Name = "TIM the Beaver" 86 | c.Bio = config.GetConfig().GetString("tim.bio") 87 | c.School = "MIT" 88 | c.Location = "Cambridge, MA" 89 | c.GradYear = 9999 90 | c.X = 0.5 91 | c.Y = 0.75 92 | c.Room = "home" 93 | c.Role = int(Organizer) 94 | return c 95 | } 96 | 97 | type QuillProfile struct { 98 | Name string `json:"name"` 99 | School string `json:"school"` 100 | GradYear string `json:"graduationYear"` 101 | SchoolLevel string `json:"schoolLevel"` 102 | } 103 | 104 | type QuillStatus struct { 105 | Admitted bool `json:"admitted"` 106 | Confirmed bool `json:"confirmed"` 107 | } 108 | 109 | type QuillResponse struct { 110 | Email string `json:"email"` 111 | ID string `json:"id"` 112 | Profile QuillProfile `json:"profile"` 113 | Status QuillStatus `json:"status"` 114 | } 115 | 116 | func NewCharacterFromQuill(profile QuillProfile) *Character { 117 | c := new(Character) 118 | c.ID = uuid.New().String() 119 | c.Name = profile.Name 120 | c.School = profile.School 121 | c.IsCollege = profile.SchoolLevel != "high" 122 | c.GradYear, _ = strconv.Atoi(profile.GradYear) 123 | c.X = 0.5 124 | c.Y = 0.5 125 | c.Room = "home" 126 | c.Role = int(Hacker) 127 | c.EyeColor = defaultEyeColor 128 | c.SkinColor = defaultSkinColor 129 | c.ShirtColor = defaultShirtColor 130 | c.PantsColor = defaultPantsColor 131 | return c 132 | } 133 | 134 | func (c *Character) MarshalBinary() ([]byte, error) { 135 | return json.Marshal(c) 136 | } 137 | 138 | func (c *Character) UnmarshalBinary(data []byte) error { 139 | return json.Unmarshal(data, c) 140 | } 141 | -------------------------------------------------------------------------------- /db/models/element.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type ElementAction int 4 | 5 | const ( 6 | OpenJukebox ElementAction = iota + 1 7 | ) 8 | 9 | type Element struct { 10 | ID string `json:"id,omitempty"` 11 | 12 | X float64 `json:"x" redis:"x"` 13 | Y float64 `json:"y" redis:"y"` 14 | Width float64 `json:"width" redis:"width"` 15 | Path string `json:"path" redis:"path"` 16 | 17 | ChangingImagePath bool `json:"changingImagePath" redis:"changingImagePath"` 18 | ChangingPaths string `json:"changingPaths" redis:"changingPaths"` 19 | 20 | // How often to update the image, in milliseconds 21 | ChangingInterval int `json:"changingInterval" redis:"changingInterval"` 22 | 23 | ChangingRandomly bool `json:"changingRandomly" redis:"changingRandomly"` 24 | 25 | Action int `json:"action" redis:"action"` 26 | 27 | Hoverable bool `json:"hoverable" redis:"hoverable"` 28 | Toggleable bool `json:"toggleable" redis:"toggleable"` 29 | State int `json:"state" redis:"state"` 30 | } 31 | -------------------------------------------------------------------------------- /db/models/event.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Event struct { 4 | Name string `json:"name" redis:"name"` 5 | URL string `json:"url" redis:"url"` 6 | 7 | // Start time of the event, as a Unix timestamp 8 | StartTime int `json:"startTime" redis:"startTime"` 9 | 10 | // Duration of the event, in minutes 11 | Duration int `json:"duration" redis:"duration"` 12 | Type string `json:"type" redis:"type"` 13 | } 14 | -------------------------------------------------------------------------------- /db/models/hallway.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Hallway struct { 4 | X float64 `json:"x" redis:"x"` 5 | Y float64 `json:"y" redis:"y"` 6 | ToX float64 `json:"toX" redis:"toX"` 7 | ToY float64 `json:"toY" redis:"toY"` 8 | Radius float64 `json:"radius" redis:"radius"` 9 | To string `json:"to" redis:"to"` 10 | } 11 | -------------------------------------------------------------------------------- /db/models/location.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Location struct { 4 | Lat float64 `json:"lat" redis:"lat"` 5 | Lng float64 `json:"lng" redis:"lng"` 6 | Name string `json:"name" redis:"name"` 7 | } 8 | -------------------------------------------------------------------------------- /db/models/log.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | ) 7 | 8 | type Log struct { 9 | CharacterID string `json:"characterId" redis:"characterId"` 10 | Message string `json:"message" redis:"message"` 11 | Timestamp int64 `json:"timestamp" redis:"timestamp"` 12 | } 13 | 14 | func NewLog(characterID, message string) *Log { 15 | return &Log{ 16 | CharacterID: characterID, 17 | Message: message, 18 | Timestamp: time.Now().Unix(), 19 | } 20 | } 21 | 22 | func (c *Log) MarshalBinary() ([]byte, error) { 23 | return json.Marshal(c) 24 | } 25 | 26 | func (c *Log) UnmarshalBinary(data []byte) error { 27 | return json.Unmarshal(data, c) 28 | } 29 | -------------------------------------------------------------------------------- /db/models/message.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Message struct { 4 | From string `json:"from" redis:"from"` 5 | Text string `json:"text" redis:"text"` 6 | To string `json:"to" redis:"to"` 7 | } 8 | -------------------------------------------------------------------------------- /db/models/project.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "encoding/json" 4 | 5 | type Project struct { 6 | Challenges string `json:"challenges" redis:"challenges"` 7 | Emails string `json:"emails" redis:"emails"` 8 | Name string `json:"name" redis:"name"` 9 | Pitch string `json:"pitch" redis:"pitch"` 10 | SubmittedAt int `json:"submittedAt" redis:"submittedAt"` 11 | Track string `json:"track" redis:"track"` 12 | Zoom string `json:"zoom" redis:"zoom"` 13 | } 14 | 15 | func (p Project) MarshalBinary() ([]byte, error) { 16 | return json.Marshal(p) 17 | } 18 | 19 | func (p Project) UnmarshalBinary(data []byte) error { 20 | return json.Unmarshal(data, p) 21 | } 22 | -------------------------------------------------------------------------------- /db/models/queue_subscriber.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | type QueueSubscriber struct { 9 | ID string `json:"id"` 10 | Name string `json:"name" redis:"name"` 11 | School string `json:"school" redis:"school"` 12 | GradYear int `json:"gradYear" redis:"gradYear"` 13 | Interests string `json:"interests" redis:"interests"` 14 | } 15 | 16 | func NewQueueSubscriber(c *Character, interests []string) *QueueSubscriber { 17 | return &QueueSubscriber{ 18 | ID: c.ID, 19 | Name: c.Name, 20 | School: c.School, 21 | GradYear: c.GradYear, 22 | Interests: strings.Join(interests, ","), 23 | } 24 | } 25 | 26 | func (s *QueueSubscriber) MarshalBinary() ([]byte, error) { 27 | return json.Marshal(s) 28 | } 29 | 30 | func (s *QueueSubscriber) UnmarshalBinary(data []byte) error { 31 | return json.Unmarshal(data, s) 32 | } 33 | -------------------------------------------------------------------------------- /db/models/room.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Room struct { 8 | Characters map[string]*Character `json:"characters" redis:"-"` 9 | Elements []*Element `json:"elements" redis:"-"` 10 | Hallways map[string]*Hallway `json:"hallways" redis:"-"` 11 | 12 | Background string `json:"background" redis:"background"` 13 | Corners string `json:"corners" redis:"corners"` 14 | ID string `json:"id" redis:"id"` 15 | SponsorID string `json:"sponsorId" redis:"sponsorId"` 16 | 17 | Sponsor *Sponsor `json:"sponsor,omitempty"` 18 | } 19 | 20 | func NewRoom(id, background string, sponsorID string) *Room { 21 | return &Room{ 22 | Characters: map[string]*Character{}, 23 | Elements: []*Element{}, 24 | Hallways: map[string]*Hallway{}, 25 | Background: background, 26 | ID: id, 27 | SponsorID: sponsorID, 28 | } 29 | } 30 | 31 | func (r *Room) Init() *Room { 32 | r.Characters = map[string]*Character{} 33 | r.Elements = []*Element{} 34 | r.Hallways = map[string]*Hallway{} 35 | return r 36 | } 37 | 38 | func (r Room) MarshalBinary() ([]byte, error) { 39 | return json.Marshal(r) 40 | } 41 | 42 | func (r Room) UnmarshalBinary(data []byte) error { 43 | return json.Unmarshal(data, r) 44 | } 45 | -------------------------------------------------------------------------------- /db/models/settings.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Settings struct { 8 | MusicMuted bool `json:"musicMuted" redis:"musicMuted"` 9 | SoundMuted bool `json:"soundMuted" redis:"soundMuted"` 10 | TwitterHandle string `json:"twitterHandle" redis:"twitterHandle"` 11 | } 12 | 13 | func (s Settings) MarshalBinary() ([]byte, error) { 14 | return json.Marshal(s) 15 | } 16 | 17 | func (s Settings) UnmarshalBinary(data []byte) error { 18 | return json.Unmarshal(data, s) 19 | } 20 | -------------------------------------------------------------------------------- /db/models/song.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Song struct { 8 | Duration int `json:"duration" redis:"duration"` 9 | ThumbnailURL string `json:"thumbnailUrl" redis:"thumbnailUrl"` 10 | Title string `json:"title" redis:"title"` 11 | VidCode string `json:"vidCode" redis:"vidCode"` 12 | ID string `json:"id" redis:"-"` 13 | } 14 | 15 | func (s *Song) Init() *Song { 16 | return s 17 | } 18 | 19 | func (s Song) MarshalBinary() ([]byte, error) { 20 | return json.Marshal(s) 21 | } 22 | 23 | func (s Song) UnmarshalBinary(data []byte) error { 24 | return json.Unmarshal(data, s) 25 | } 26 | -------------------------------------------------------------------------------- /db/models/sponsor.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type Sponsor struct { 8 | Challenges string `json:"challenges" redis:"challenges"` 9 | Description string `json:"description" redis:"description"` 10 | URL string `json:"url" redis:"url"` 11 | Name string `json:"name" redis:"name"` 12 | ID string `json:"id" redis:"-"` 13 | QueueOpen bool `json:"queueOpen" redis:"queueOpen"` 14 | } 15 | 16 | func (s Sponsor) MarshalBinary() ([]byte, error) { 17 | return json.Marshal(s) 18 | } 19 | 20 | func (s Sponsor) UnmarshalBinary(data []byte) error { 21 | return json.Unmarshal(data, s) 22 | } 23 | -------------------------------------------------------------------------------- /db/reset.go: -------------------------------------------------------------------------------- 1 | package db 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "strings" 7 | "time" 8 | 9 | "github.com/google/uuid" 10 | "github.com/techx/playground/config" 11 | ) 12 | 13 | // RoomType is an enum representing all possible room templates 14 | type RoomType string 15 | 16 | const ( 17 | // Home is the room that everyone spawns in, otherwise known as town square 18 | Home RoomType = "home" 19 | 20 | // Plaza is the room where you can get to the coffee shop, arcade, and stadium 21 | Plaza = "plaza" 22 | 23 | // Nightclub is the club, accessible from town square 24 | Nightclub = "nightclub" 25 | 26 | // CoffeeShop is the coffee shop, accessible from plaza 27 | CoffeeShop = "coffee_shop" 28 | 29 | // Nonprofits is the campground with all of the nonprofit tents 30 | Nonprofits = "nonprofits" 31 | 32 | // Personal is a template for someone's personal room 33 | Personal = "personal" 34 | 35 | // PlatArea is the area accessible from town square with the two plat sponsor buildings 36 | PlatArea = "plat_area" 37 | 38 | // LeftField is the left sponsor area 39 | LeftField = "left_field" 40 | 41 | // RightField is the right sponsor area 42 | RightField = "right_field" 43 | 44 | // Plat is a plat-tier sponsor's room 45 | Plat = "plat" 46 | 47 | // Gold is a gold-tier sponsor's room 48 | Gold = "gold" 49 | 50 | // Silver is a silver-tier sponsor's room 51 | Silver = "silver" 52 | 53 | // Bronze is a bronze-tier sponsor's room 54 | Bronze = "bronze" 55 | 56 | // Arena is the hacking arena, accessible from town square 57 | Arena = "arena" 58 | 59 | // Mall is the clothing store, accessible from town square 60 | Mall = "mall" 61 | 62 | // MISTI is the room for MISTI, accessible from the plaza 63 | MISTI = "misti" 64 | 65 | // Auditorium is the room for the stadium, accessible from the plaza 66 | Auditorium = "auditorium" 67 | ) 68 | 69 | // CreateRoom builds a room with the given ID from a template file 70 | func createRoomWithData(id string, roomType RoomType, data map[string]interface{}) { 71 | dat, err := ioutil.ReadFile("config/rooms/" + string(roomType) + ".json") 72 | 73 | if err != nil { 74 | return 75 | } 76 | 77 | var roomData map[string]interface{} 78 | json.Unmarshal(dat, &roomData) 79 | data["background"] = roomData["background"] 80 | 81 | if val, ok := roomData["corners"]; ok { 82 | data["corners"] = val 83 | } 84 | 85 | if sponsorID, ok := data["id"].(string); ok { 86 | data["background"] = strings.ReplaceAll(data["background"].(string), "", sponsorID) 87 | 88 | if val, ok := roomData["sponsor"].(bool); ok && val { 89 | data["sponsorId"] = sponsorID 90 | } 91 | } 92 | 93 | instance.HSet("room:"+id, data) 94 | 95 | elements := roomData["elements"].([]interface{}) 96 | 97 | // If this is the nightclub, add floor tiles 98 | if id == "nightclub" { 99 | tileStartX := 0.374 100 | tileStartY := 0.552 101 | tileSeparator := 0.0305 102 | numTilesX := 7 103 | numTilesY := 4 104 | 105 | newTiles := make([]interface{}, numTilesX*numTilesY) 106 | 107 | for i := 0; i < numTilesY; i++ { 108 | for j := 0; j < numTilesX; j++ { 109 | newTiles[i*numTilesX+j] = map[string]interface{}{ 110 | "x": tileStartX + float64(i+j)*tileSeparator, 111 | "y": tileStartY + float64((numTilesY-i)+j)*tileSeparator, 112 | "tile": true, 113 | } 114 | } 115 | } 116 | 117 | elements = append(newTiles, elements...) 118 | } 119 | 120 | for _, val := range elements { 121 | elementID := uuid.New().String() 122 | elementData := val.(map[string]interface{}) 123 | 124 | if _, ok := elementData["tile"]; ok { 125 | // If this is a nightclub floor tile, autofill some attributes 126 | delete(elementData, "tile") 127 | elementData["width"] = 0.052 128 | elementData["path"] = "tiles/blue1.svg" 129 | elementData["changingImagePath"] = true 130 | elementData["changingPaths"] = "tiles/blue1.svg,tiles/blue2.svg,tiles/blue3.svg,tiles/blue4.svg,tiles/green1.svg,tiles/green2.svg,tiles/pink1.svg,tiles/pink2.svg,tiles/pink3.svg,tiles/pink4.svg,tiles/yellow1.svg" 131 | elementData["changingInterval"] = 2000 132 | elementData["changingRandomly"] = true 133 | } 134 | 135 | if _, ok := elementData["campfire"]; ok { 136 | // If this is a campfire, animate it 137 | delete(elementData, "campfire") 138 | elementData["width"] = 0.0253 139 | elementData["path"] = "campfire/campfire1.svg" 140 | elementData["changingImagePath"] = true 141 | elementData["changingPaths"] = "campfire/campfire1.svg,campfire/campfire2.svg,campfire/campfire3.svg,campfire/campfire4.svg,campfire/campfire5.svg" 142 | elementData["changingInterval"] = 250 143 | elementData["changingRandomly"] = false 144 | } 145 | 146 | if _, ok := elementData["fountain"]; ok { 147 | // If this is a fountain, animate it 148 | delete(elementData, "fountain") 149 | elementData["path"] = "fountain1.svg" 150 | elementData["changingImagePath"] = true 151 | elementData["changingPaths"] = "fountain1.svg,fountain2.svg,fountain3.svg" 152 | elementData["changingInterval"] = 1000 153 | elementData["changingRandomly"] = false 154 | } 155 | 156 | if _, ok := elementData["toggleable"]; ok { 157 | switch elementData["path"] { 158 | case "street_lamp.svg": 159 | elementData["path"] = "street_lamp.svg,street_lamp_off.svg" 160 | case "bar_closed.svg": 161 | elementData["path"] = "bar_closed.svg,bar_open.svg" 162 | case "flashlight_off.svg": 163 | elementData["path"] = "flashlight_off.svg,flashlight_on.svg" 164 | default: 165 | break 166 | } 167 | 168 | elementData["state"] = 0 169 | } 170 | 171 | if id, ok := data["id"].(string); ok { 172 | elementData["path"] = strings.ReplaceAll(elementData["path"].(string), "", id) 173 | } 174 | 175 | instance.HSet("element:"+elementID, elementData) 176 | instance.RPush("room:"+id+":elements", elementID) 177 | } 178 | 179 | for _, val := range roomData["hallways"].([]interface{}) { 180 | hallwayData := val.(map[string]interface{}) 181 | 182 | if roomType == Bronze || roomType == Silver || roomType == Gold || roomType == Plat { 183 | hallwayData["toX"] = data["toX"].(float64) 184 | hallwayData["toY"] = data["toY"].(float64) 185 | 186 | if val, ok := data["to"].(string); ok { 187 | hallwayData["to"] = val 188 | } 189 | } 190 | 191 | hallwayID := uuid.New().String() 192 | instance.HSet("hallway:"+hallwayID, val) 193 | instance.SAdd("room:"+id+":hallways", hallwayID) 194 | } 195 | 196 | instance.SAdd("rooms", id) 197 | } 198 | 199 | func createSponsors() { 200 | dat, err := ioutil.ReadFile("config/sponsors.json") 201 | 202 | if err != nil { 203 | return 204 | } 205 | 206 | var sponsorsData []map[string]interface{} 207 | json.Unmarshal(dat, &sponsorsData) 208 | 209 | for _, sponsor := range sponsorsData { 210 | sponsorID := sponsor["id"].(string) 211 | delete(sponsor, "id") 212 | 213 | instance.HSet("sponsor:"+sponsorID, sponsor) 214 | instance.SAdd("sponsors", sponsorID) 215 | } 216 | } 217 | 218 | func CreateRoom(id string, roomType RoomType) { 219 | createRoomWithData(id, roomType, map[string]interface{}{}) 220 | } 221 | 222 | func createEvents() { 223 | dat, err := ioutil.ReadFile("config/events.json") 224 | 225 | if err != nil { 226 | return 227 | } 228 | 229 | var eventsData []map[string]interface{} 230 | json.Unmarshal(dat, &eventsData) 231 | 232 | for _, event := range eventsData { 233 | startTime, err := time.Parse("2006-01-02T15:04:05-0700", event["start_time"].(string)) 234 | 235 | if err != nil { 236 | panic(err) 237 | } 238 | 239 | event["startTime"] = int(startTime.Unix()) 240 | 241 | eventID := uuid.New().String()[:4] 242 | instance.HSet("event:"+eventID, event) 243 | instance.SAdd("events", eventID) 244 | } 245 | } 246 | 247 | func reset() { 248 | instance.FlushDB() 249 | CreateRoom("home", Home) 250 | CreateRoom("nightclub", Nightclub) 251 | CreateRoom("nonprofits", Nonprofits) 252 | CreateRoom("plat_area", PlatArea) 253 | CreateRoom("left_field", LeftField) 254 | CreateRoom("right_field", RightField) 255 | CreateRoom("plaza", Plaza) 256 | CreateRoom("coffee_shop", CoffeeShop) 257 | CreateRoom("mall", Mall) 258 | CreateRoom("auditorium", Auditorium) 259 | 260 | createRoomWithData("arena:connectivity", Arena, map[string]interface{}{ 261 | "id": "connectivity", 262 | }) 263 | 264 | createRoomWithData("arena:education", Arena, map[string]interface{}{ 265 | "id": "education", 266 | }) 267 | 268 | createRoomWithData("arena:health", Arena, map[string]interface{}{ 269 | "id": "health", 270 | }) 271 | 272 | createRoomWithData("arena:urban", Arena, map[string]interface{}{ 273 | "id": "urban", 274 | }) 275 | 276 | createRoomWithData("sponsor:beaverworks", Gold, map[string]interface{}{ 277 | "id": "beaverworks", 278 | "to": "left_field", 279 | "toX": 0.3042, 280 | "toY": 0.6834, 281 | }) 282 | 283 | createRoomWithData("sponsor:ieee", Gold, map[string]interface{}{ 284 | "id": "ieee", 285 | "to": "left_field", 286 | "toX": 0.1137, 287 | "toY": 0.4796, 288 | }) 289 | 290 | createRoomWithData("sponsor:kodewithklossy", Gold, map[string]interface{}{ 291 | "id": "kodewithklossy", 292 | "to": "left_field", 293 | "toX": 0.6958, 294 | "toY": 0.6834, 295 | }) 296 | 297 | createRoomWithData("sponsor:ktbyte", Gold, map[string]interface{}{ 298 | "id": "ktbyte", 299 | "to": "left_field", 300 | "toX": 0.8969, 301 | "toY": 0.4657, 302 | }) 303 | 304 | createRoomWithData("sponsor:leah", Gold, map[string]interface{}{ 305 | "id": "leah", 306 | "to": "right_field", 307 | "toX": 0.1, 308 | "toY": 0.4598, 309 | }) 310 | 311 | createRoomWithData("sponsor:lsa", Gold, map[string]interface{}{ 312 | "id": "lsa", 313 | "to": "right_field", 314 | "toX": 0.3108, 315 | "toY": 0.6903, 316 | }) 317 | 318 | createRoomWithData("sponsor:medscience", Gold, map[string]interface{}{ 319 | "id": "medscience", 320 | "to": "right_field", 321 | "toX": 0.7037, 322 | "toY": 0.6647, 323 | }) 324 | 325 | createRoomWithData("sponsor:lincoln", Gold, map[string]interface{}{ 326 | "id": "lincoln", 327 | "to": "right_field", 328 | "toX": 0.9152, 329 | "toY": 0.4563, 330 | }) 331 | 332 | createRoomWithData("sponsor:misti", MISTI, map[string]interface{}{ 333 | "id": "misti", 334 | }) 335 | 336 | createEvents() 337 | createSponsors() 338 | 339 | if len(config.GetSecret("EMAIL")) > 0 { 340 | instance.SAdd("organizer_emails", config.GetSecret("EMAIL")) 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/techx/playground 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/Masterminds/goutils v1.1.0 // indirect 7 | github.com/Masterminds/semver v1.5.0 // indirect 8 | github.com/Masterminds/sprig v2.22.0+incompatible // indirect 9 | github.com/SherClockHolmes/webpush-go v1.1.2 10 | github.com/andybalholm/cascadia v1.2.0 // indirect 11 | github.com/aokoli/goutils v1.1.0 // indirect 12 | github.com/aws/aws-sdk-go v1.34.6 13 | github.com/deckarep/golang-set v1.7.1 14 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 15 | github.com/go-redis/redis/v7 v7.4.0 16 | github.com/google/uuid v1.1.2 17 | github.com/gorilla/websocket v1.4.2 18 | github.com/huandu/xstrings v1.3.2 // indirect 19 | github.com/imdario/mergo v0.3.11 // indirect 20 | github.com/jaytaylor/html2text v0.0.0-20200412013138-3577fbdbcff7 // indirect 21 | github.com/joho/godotenv v1.3.0 22 | github.com/labstack/echo/v4 v4.1.16 23 | github.com/matcornic/hermes/v2 v2.1.0 24 | github.com/mattn/go-runewidth v0.0.9 // indirect 25 | github.com/mitchellh/copystructure v1.0.0 // indirect 26 | github.com/mitchellh/reflectwalk v1.0.1 // indirect 27 | github.com/olekukonko/tablewriter v0.0.4 // indirect 28 | github.com/spf13/viper v1.7.1 29 | github.com/vanng822/go-premailer v1.9.0 // indirect 30 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect 31 | google.golang.org/api v0.30.0 32 | ) 33 | -------------------------------------------------------------------------------- /img/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HackMIT/playground/b3031ee533ac62e78a41466f4ccd945cec132913/img/banner.png -------------------------------------------------------------------------------- /server/router.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | 7 | "github.com/techx/playground/controllers" 8 | "github.com/techx/playground/socket" 9 | 10 | "github.com/labstack/echo/v4" 11 | "github.com/labstack/echo/v4/middleware" 12 | ) 13 | 14 | func newRouter(hub *socket.Hub) *echo.Echo { 15 | e := echo.New() 16 | 17 | // Define middlewares 18 | e.Use(middleware.CORS()) 19 | e.Use(middleware.Logger()) 20 | e.Use(middleware.Recover()) 21 | 22 | if os.Getenv("PRODUCTION") == "true" { 23 | e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { 24 | return func(c echo.Context) error { 25 | req := c.Request() 26 | 27 | if req.Header.Get("X-Forwarded-Proto") == "http" { 28 | return c.Redirect(http.StatusMovedPermanently, "https://"+req.Host+req.RequestURI) 29 | } 30 | 31 | return next(c) 32 | } 33 | }) 34 | } 35 | 36 | // Rooms controller 37 | room := new(controllers.RoomController) 38 | e.GET("/rooms", room.GetRooms) 39 | 40 | return e 41 | } 42 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/techx/playground/db" 8 | "github.com/techx/playground/socket" 9 | ) 10 | 11 | func Init(port string) { 12 | hub := new(socket.Hub).Init() 13 | 14 | // Wait for socket messages 15 | go hub.Run() 16 | 17 | // Listen for events from other ingest servers 18 | go db.ListenForUpdates(hub.ProcessRedisMessage) 19 | 20 | // Check if we're the leader and do things if so 21 | go db.MonitorLeader() 22 | 23 | // Websocket connection endpoint 24 | http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) { 25 | socket.ServeWs(hub, w, r) 26 | }) 27 | 28 | // REST endpoints 29 | r := newRouter(hub) 30 | http.Handle("/", r) 31 | 32 | addr := ":" + port 33 | 34 | // Start the server 35 | fmt.Println("Serving at", addr) 36 | err := http.ListenAndServe(addr, nil) 37 | 38 | if err != nil { 39 | panic(err) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /socket/client.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "github.com/techx/playground/db/models" 5 | 6 | "github.com/google/uuid" 7 | "github.com/gorilla/websocket" 8 | ) 9 | 10 | // Client is a middleman between the websocket connection and the hub. 11 | type Client struct { 12 | hub *Hub 13 | 14 | // The websocket connection. 15 | conn *websocket.Conn 16 | 17 | // Buffered channel of outbound messages. 18 | send chan []byte 19 | 20 | // ID uniquely identifying this client 21 | id string 22 | 23 | // This client's character 24 | character *models.Character 25 | } 26 | 27 | func NewClient(hub *Hub, conn *websocket.Conn) *Client { 28 | c := new(Client) 29 | c.hub = hub 30 | c.conn = conn 31 | c.send = make(chan []byte, 4096) 32 | c.id = uuid.New().String() 33 | return c 34 | } 35 | -------------------------------------------------------------------------------- /socket/message.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // SocketMessage stores messages sent over WS with the client who sent it 8 | type SocketMessage struct { 9 | msg []byte 10 | sender *Client 11 | } 12 | 13 | func (m SocketMessage) MarshalBinary() ([]byte, error) { 14 | return json.Marshal(m) 15 | } 16 | 17 | func (m SocketMessage) UnmarshalBinary(data []byte) error { 18 | return json.Unmarshal(data, m) 19 | } 20 | -------------------------------------------------------------------------------- /socket/packet/README.md: -------------------------------------------------------------------------------- 1 | # packet 2 | 3 | This is where we keep all of the packets that can be sent in Playground! Our packets generally fall into two classes (and they can belong to both): 4 | - **client**: A packet sent by clients to the server 5 | - e.g. `get_messages` will be sent by a client who needs to load their private messages with someone else 6 | - **server**: A packet sent by the server to clients 7 | - e.g. `messages` will be returned to a client after they send a `get_messages` packet 8 | 9 | One example of a packet that belongs to both classes is `chat`, which a client can send to the server in order to send a chat message to the room. The server will in turn send this same packet back to the other clients. 10 | 11 | ## Adding a new client packet 12 | 1. Create a struct for this packet (see existing files for examples). Make sure to implement `PermissionCheck`, `MarshalBinary`, and `UnmarshalBinary`. 13 | - `PermissionCheck` returns true when the sender has permission to send that packet 14 | 2. Add the new packet to `parse.go` along with its identifier 15 | 3. Write the handler for this packet in hub.go 16 | 17 | ## Adding a new server packet 18 | 1. Create a struct for this packet. Make sure to implement `MarshalBinary` and `UnmarshalBinary` 19 | 2. Create an `Init` function with parameters that mirror what needs to be sent to the client 20 | 3. Use the `Init` function to create a new packet, and pass it to `h.Send` to send it to clients -------------------------------------------------------------------------------- /socket/packet/achievements.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db" 7 | "github.com/techx/playground/db/models" 8 | "github.com/techx/playground/utils" 9 | ) 10 | 11 | type AchievementsPacket struct { 12 | BasePacket 13 | Packet `json:",omitempty"` 14 | models.Achievements `json:"achievements"` 15 | 16 | // The id of the client who we're getting achievements for 17 | ID string `json:"id"` 18 | } 19 | 20 | func NewAchievementsPacket(characterID string) *AchievementsPacket { 21 | p := new(AchievementsPacket) 22 | p.BasePacket = BasePacket{ 23 | Type: "achievements", 24 | } 25 | 26 | p.ID = characterID 27 | 28 | res, _ := db.GetInstance().HGetAll("character:" + characterID + ":achievements").Result() 29 | utils.Bind(res, &p.Achievements) 30 | 31 | return p 32 | } 33 | 34 | func (p AchievementsPacket) MarshalBinary() ([]byte, error) { 35 | return json.Marshal(p) 36 | } 37 | 38 | func (p AchievementsPacket) UnmarshalBinary(data []byte) error { 39 | return json.Unmarshal(data, p) 40 | } 41 | -------------------------------------------------------------------------------- /socket/packet/add_email.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they need to add a valid email to our system 10 | type AddEmailPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The email address to check and send the code to 15 | Email string `json:"email"` 16 | 17 | // The role this user is signing up for (see models.Role) 18 | Role int `json:"role"` 19 | 20 | // The id of the sponsor, if Role == SponsorRep 21 | SponsorID string `json:"sponsorId"` 22 | } 23 | 24 | func (p AddEmailPacket) PermissionCheck(characterID string, role models.Role) bool { 25 | return len(characterID) > 0 && role == models.Organizer 26 | } 27 | 28 | func (p AddEmailPacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p AddEmailPacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/base.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "github.com/techx/playground/db/models" 5 | ) 6 | 7 | type Packet interface { 8 | PermissionCheck(characterID string, role models.Role) bool 9 | } 10 | 11 | // The base packet that can be sent between clients and server. These are all 12 | // of the required attributes of any packet 13 | type BasePacket struct { 14 | Packet 15 | 16 | // Identifies the type of packet 17 | Type string `json:"type"` 18 | } 19 | 20 | func (p *BasePacket) PermissionCheck(characterID string, role models.Role) bool { 21 | return false 22 | } 23 | -------------------------------------------------------------------------------- /socket/packet/chat.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type ChatPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | // The message being sent 14 | Message string `json:"mssg"` 15 | 16 | // The id of the client who's joining 17 | ID string `json:"id"` 18 | 19 | // The client's room 20 | Room string `json:"room"` 21 | } 22 | 23 | func (p ChatPacket) PermissionCheck(characterID string, role models.Role) bool { 24 | return len(characterID) > 0 25 | } 26 | 27 | func (p ChatPacket) MarshalBinary() ([]byte, error) { 28 | return json.Marshal(p) 29 | } 30 | 31 | func (p ChatPacket) UnmarshalBinary(data []byte) error { 32 | return json.Unmarshal(data, p) 33 | } 34 | -------------------------------------------------------------------------------- /socket/packet/dance.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they dance 10 | type DancePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The id of the client who is dancing 15 | ID string `json:"id"` 16 | 17 | // The room that the client is dancing in 18 | Room string `json:"room"` 19 | 20 | // The client's new x position (0-1) 21 | Dance int `json:"dance"` 22 | } 23 | 24 | func (p DancePacket) PermissionCheck(characterID string, role models.Role) bool { 25 | return len(characterID) > 0 26 | } 27 | 28 | func (p DancePacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p DancePacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/element_add.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they're adding an element 10 | type ElementAddPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The room being updated 15 | Room string `json:"room"` 16 | 17 | // The ID of the element being updated 18 | ID string `json:"id"` 19 | 20 | // The new element 21 | Element models.Element `json:"element"` 22 | } 23 | 24 | func (p ElementAddPacket) PermissionCheck(characterID string, role models.Role) bool { 25 | return len(characterID) > 0 && role == models.Organizer 26 | } 27 | 28 | func (p ElementAddPacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p ElementAddPacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/element_delete.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they're deleting an element 10 | type ElementDeletePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The room being updated 15 | Room string `json:"room"` 16 | 17 | // The ID of the element being deleted 18 | ID string `json:"id"` 19 | } 20 | 21 | func (p ElementDeletePacket) PermissionCheck(characterID string, role models.Role) bool { 22 | return len(characterID) > 0 && role == models.Organizer 23 | } 24 | 25 | func (p ElementDeletePacket) MarshalBinary() ([]byte, error) { 26 | return json.Marshal(p) 27 | } 28 | 29 | func (p ElementDeletePacket) UnmarshalBinary(data []byte) error { 30 | return json.Unmarshal(data, p) 31 | } 32 | -------------------------------------------------------------------------------- /socket/packet/element_toggle.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they're deleting an element 10 | type ElementTogglePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The ID of the element being toggled 15 | ID string `json:"id"` 16 | } 17 | 18 | func (p ElementTogglePacket) PermissionCheck(characterID string, role models.Role) bool { 19 | return len(characterID) > 0 20 | } 21 | 22 | func (p ElementTogglePacket) MarshalBinary() ([]byte, error) { 23 | return json.Marshal(p) 24 | } 25 | 26 | func (p ElementTogglePacket) UnmarshalBinary(data []byte) error { 27 | return json.Unmarshal(data, p) 28 | } 29 | -------------------------------------------------------------------------------- /socket/packet/element_update.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they're updating the room 10 | type ElementUpdatePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The room being updated 15 | Room string `json:"room"` 16 | 17 | // The ID of the element being updated 18 | ID string `json:"id"` 19 | 20 | // The new element 21 | Element models.Element `json:"element"` 22 | } 23 | 24 | func NewElementUpdatePacket(room, id string, element models.Element) *ElementUpdatePacket { 25 | return &ElementUpdatePacket{ 26 | BasePacket: BasePacket{ 27 | Type: "element_update", 28 | }, 29 | Room: room, 30 | ID: id, 31 | Element: element, 32 | } 33 | } 34 | 35 | func (p ElementUpdatePacket) PermissionCheck(characterID string, role models.Role) bool { 36 | return len(characterID) > 0 && role == models.Organizer 37 | } 38 | 39 | func (p ElementUpdatePacket) MarshalBinary() ([]byte, error) { 40 | return json.Marshal(p) 41 | } 42 | 43 | func (p ElementUpdatePacket) UnmarshalBinary(data []byte) error { 44 | return json.Unmarshal(data, p) 45 | } 46 | -------------------------------------------------------------------------------- /socket/packet/email_code.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they need a login code 10 | type EmailCodePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The email address to check and send the code to 15 | Email string `json:"email"` 16 | 17 | // The role this user is signing up for (see models.Role) 18 | Role int `json:"role"` 19 | } 20 | 21 | func (p EmailCodePacket) PermissionCheck(characterID string, role models.Role) bool { 22 | return true 23 | } 24 | 25 | func (p EmailCodePacket) MarshalBinary() ([]byte, error) { 26 | return json.Marshal(p) 27 | } 28 | 29 | func (p EmailCodePacket) UnmarshalBinary(data []byte) error { 30 | return json.Unmarshal(data, p) 31 | } 32 | -------------------------------------------------------------------------------- /socket/packet/error.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type ErrorPacket struct { 8 | BasePacket 9 | Packet `json:",omitempty"` 10 | 11 | Code int `json:"code"` 12 | } 13 | 14 | func NewErrorPacket(code int) *ErrorPacket { 15 | p := new(ErrorPacket) 16 | p.BasePacket = BasePacket{Type: "error"} 17 | p.Code = code 18 | return p 19 | } 20 | 21 | func (p ErrorPacket) MarshalBinary() ([]byte, error) { 22 | return json.Marshal(p) 23 | } 24 | 25 | func (p ErrorPacket) UnmarshalBinary(data []byte) error { 26 | return json.Unmarshal(data, p) 27 | } 28 | -------------------------------------------------------------------------------- /socket/packet/event.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent when a user confirms attendance for an event 10 | type EventPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // ID of this event 15 | ID string `json:"id"` 16 | } 17 | 18 | func (p *EventPacket) Init(id string) *EventPacket { 19 | p.BasePacket = BasePacket{Type: "event"} 20 | p.ID = id 21 | return p 22 | } 23 | 24 | func (p EventPacket) PermissionCheck(characterID string, role models.Role) bool { 25 | return len(characterID) > 0 26 | } 27 | 28 | func (p EventPacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p EventPacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/friend_request.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type FriendRequestPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | SenderID string `json:"senderId"` 14 | RecipientID string `json:"recipientId"` 15 | } 16 | 17 | func (p FriendRequestPacket) PermissionCheck(characterID string, role models.Role) bool { 18 | return len(characterID) > 0 19 | } 20 | 21 | func (p FriendRequestPacket) MarshalBinary() ([]byte, error) { 22 | return json.Marshal(p) 23 | } 24 | 25 | func (p FriendRequestPacket) UnmarshalBinary(data []byte) error { 26 | return json.Unmarshal(data, p) 27 | } 28 | -------------------------------------------------------------------------------- /socket/packet/friend_update.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/techx/playground/db" 8 | "github.com/techx/playground/db/models" 9 | "github.com/techx/playground/utils" 10 | ) 11 | 12 | type Friend struct { 13 | ID string `json:"id"` 14 | Name string `json:"name"` 15 | School string `json:"school"` 16 | Status int `json:"status"` 17 | Teammate bool `json:"teammate"` 18 | Pending bool `json:"pending"` 19 | LastSeen time.Time `json:"lastSeen"` 20 | } 21 | 22 | type FriendUpdatePacket struct { 23 | BasePacket 24 | Packet `json:",omitempty"` 25 | 26 | Friend Friend `json:"friend"` 27 | 28 | // Server attributes 29 | RecipientID string `json:"recipientId"` 30 | } 31 | 32 | func NewFriendUpdatePacket(characterID string, friendID string) *FriendUpdatePacket { 33 | pip := db.GetInstance().Pipeline() 34 | friendCmd := pip.HGetAll("character:" + friendID) 35 | isTeammateCmd := pip.SIsMember("character:"+characterID+":teammates", friendID) 36 | isRequestCmd := pip.SIsMember("character:"+characterID+":requests", friendID) 37 | pip.Exec() 38 | 39 | friendData, _ := friendCmd.Result() 40 | friend := new(models.Character) 41 | utils.Bind(friendData, friend) 42 | 43 | isTeammate, _ := isTeammateCmd.Result() 44 | isRequest, _ := isRequestCmd.Result() 45 | 46 | return &FriendUpdatePacket{ 47 | BasePacket: BasePacket{ 48 | Type: "friend_update", 49 | }, 50 | Friend: Friend{ 51 | ID: friendID, 52 | Name: friend.Name, 53 | School: friend.School, 54 | Status: 0, 55 | Teammate: isTeammate, 56 | Pending: isRequest, 57 | LastSeen: time.Now(), 58 | }, 59 | RecipientID: characterID, 60 | } 61 | } 62 | 63 | // This isn't needed -- remove later 64 | func (p FriendUpdatePacket) PermissionCheck(characterID string, role models.Role) bool { 65 | return len(characterID) > 0 66 | } 67 | 68 | func (p FriendUpdatePacket) MarshalBinary() ([]byte, error) { 69 | return json.Marshal(p) 70 | } 71 | 72 | func (p FriendUpdatePacket) UnmarshalBinary(data []byte) error { 73 | return json.Unmarshal(data, p) 74 | } 75 | -------------------------------------------------------------------------------- /socket/packet/get_achievements.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type GetAchievementsPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | ID string `json:"id"` 14 | } 15 | 16 | func (p GetAchievementsPacket) PermissionCheck(characterID string, role models.Role) bool { 17 | return len(characterID) > 0 18 | } 19 | 20 | func (p GetAchievementsPacket) MarshalBinary() ([]byte, error) { 21 | return json.Marshal(p) 22 | } 23 | 24 | func (p GetAchievementsPacket) UnmarshalBinary(data []byte) error { 25 | return json.Unmarshal(data, p) 26 | } 27 | -------------------------------------------------------------------------------- /socket/packet/get_current_song.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type GetCurrentSongPacket struct { 10 | BasePacket 11 | } 12 | 13 | func (p GetCurrentSongPacket) PermissionCheck(characterID string, role models.Role) bool { 14 | return len(characterID) > 0 15 | } 16 | 17 | func (p GetCurrentSongPacket) MarshalBinary() ([]byte, error) { 18 | return json.Marshal(p) 19 | } 20 | 21 | func (p GetCurrentSongPacket) UnmarshalBinary(data []byte) error { 22 | return json.Unmarshal(data, p) 23 | } -------------------------------------------------------------------------------- /socket/packet/get_map.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import "github.com/techx/playground/db/models" 4 | 5 | type GetMapPacket struct { 6 | BasePacket 7 | } 8 | 9 | func (p GetMapPacket) PermissionCheck(characterID string, role models.Role) bool { 10 | return len(characterID) > 0 11 | } 12 | -------------------------------------------------------------------------------- /socket/packet/get_messages.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type GetMessagesPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | Recipient string `json:"recipient"` 14 | } 15 | 16 | func (p GetMessagesPacket) PermissionCheck(characterID string, role models.Role) bool { 17 | return len(characterID) > 0 18 | } 19 | 20 | func (p GetMessagesPacket) MarshalBinary() ([]byte, error) { 21 | return json.Marshal(p) 22 | } 23 | 24 | func (p GetMessagesPacket) UnmarshalBinary(data []byte) error { 25 | return json.Unmarshal(data, p) 26 | } 27 | -------------------------------------------------------------------------------- /socket/packet/get_songs.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type GetSongsPacket struct { 10 | BasePacket 11 | } 12 | 13 | func (p GetSongsPacket) PermissionCheck(characterID string, role models.Role) bool { 14 | return len(characterID) > 0 15 | } 16 | 17 | func (p GetSongsPacket) MarshalBinary() ([]byte, error) { 18 | return json.Marshal(p) 19 | } 20 | 21 | func (p GetSongsPacket) UnmarshalBinary(data []byte) error { 22 | return json.Unmarshal(data, p) 23 | } -------------------------------------------------------------------------------- /socket/packet/get_sponsor.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type GetSponsorPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | SponsorID string `json:"id"` 14 | } 15 | 16 | func (p GetSponsorPacket) PermissionCheck(characterID string, role models.Role) bool { 17 | return len(characterID) > 0 18 | } 19 | 20 | func (p GetSponsorPacket) MarshalBinary() ([]byte, error) { 21 | return json.Marshal(p) 22 | } 23 | 24 | func (p GetSponsorPacket) UnmarshalBinary(data []byte) error { 25 | return json.Unmarshal(data, p) 26 | } 27 | -------------------------------------------------------------------------------- /socket/packet/hallway_add.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when adding a hallway 10 | type HallwayAddPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The room being updated 15 | Room string `json:"room"` 16 | 17 | // The ID of the hallway being updated 18 | ID string `json:"id"` 19 | 20 | // The new hallway 21 | Hallway models.Hallway `json:"hallway"` 22 | } 23 | 24 | func (p HallwayAddPacket) PermissionCheck(characterID string, role models.Role) bool { 25 | return len(characterID) > 0 && role == models.Organizer 26 | } 27 | 28 | func (p HallwayAddPacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p HallwayAddPacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/hallway_delete.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they're deleting a hallway 10 | type HallwayDeletePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The room being updated 15 | Room string `json:"room"` 16 | 17 | // The ID of the hallway being deleted 18 | ID string `json:"id"` 19 | } 20 | 21 | func (p HallwayDeletePacket) PermissionCheck(characterID string, role models.Role) bool { 22 | return len(characterID) > 0 && role == models.Organizer 23 | } 24 | 25 | func (p HallwayDeletePacket) MarshalBinary() ([]byte, error) { 26 | return json.Marshal(p) 27 | } 28 | 29 | func (p HallwayDeletePacket) UnmarshalBinary(data []byte) error { 30 | return json.Unmarshal(data, p) 31 | } 32 | -------------------------------------------------------------------------------- /socket/packet/hallway_update.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they're updating a hallway 10 | type HallwayUpdatePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The room being updated 15 | Room string `json:"room"` 16 | 17 | // The ID of the hallway being updated 18 | ID string `json:"id"` 19 | 20 | // The updated hallway 21 | Hallway models.Hallway `json:"hallway"` 22 | } 23 | 24 | func (p HallwayUpdatePacket) PermissionCheck(characterID string, role models.Role) bool { 25 | return len(characterID) > 0 && role == models.Organizer 26 | } 27 | 28 | func (p HallwayUpdatePacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p HallwayUpdatePacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/init.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "time" 7 | 8 | "github.com/techx/playground/config" 9 | "github.com/techx/playground/db" 10 | "github.com/techx/playground/db/models" 11 | "github.com/techx/playground/utils" 12 | 13 | "github.com/dgrijalva/jwt-go" 14 | "github.com/go-redis/redis/v7" 15 | ) 16 | 17 | // Sent by server to clients upon connecting. Contains information about the 18 | // world that they load into 19 | type InitPacket struct { 20 | BasePacket 21 | Packet `json:",omitempty"` 22 | 23 | Character *models.Character `json:"character"` 24 | 25 | // The room that the client is about to join 26 | Room *models.Room `json:"room"` 27 | 28 | // A token for the client to save for future authentication 29 | Token string `json:"token,omitempty"` 30 | 31 | // All possible element names 32 | ElementNames []string `json:"elementNames"` 33 | 34 | // All room names 35 | RoomNames []string `json:"roomNames"` 36 | 37 | // All of this user's friends 38 | Friends []Friend `json:"friends"` 39 | 40 | // All of the events happening throughout the weekend 41 | Events []*models.Event `json:"events"` 42 | 43 | // Projects of the users in this room, if we're in the hacking arena 44 | Projects []*models.Project `json:"projects"` 45 | 46 | // Settings 47 | Settings *models.Settings `json:"settings"` 48 | 49 | // True if the feedback window should open 50 | OpenFeedback bool `json:"openFeedback"` 51 | 52 | // True if the user needs to register 53 | FirstTime bool `json:"firstTime"` 54 | } 55 | 56 | func NewInitPacket(characterID, roomID string, needsToken bool) *InitPacket { 57 | // Fetch character and room from Redis 58 | pip := db.GetInstance().Pipeline() 59 | roomCmd := pip.HGetAll("room:" + roomID) 60 | roomCharactersCmd := pip.SMembers("room:" + roomID + ":characters") 61 | roomElementsCmd := pip.LRange("room:"+roomID+":elements", 0, -1) 62 | roomHallwaysCmd := pip.SMembers("room:" + roomID + ":hallways") 63 | characterCmd := pip.HGetAll("character:" + characterID) 64 | settingsCmd := pip.HGetAll("character:" + characterID + ":settings") 65 | teammatesCmd := pip.SMembers("character:" + characterID + ":teammates") 66 | friendsCmd := pip.SMembers("character:" + characterID + ":friends") 67 | requestsCmd := pip.SMembers("character:" + characterID + ":requests") 68 | projectIDCmd := pip.Get("character:" + characterID + ":project") 69 | eventsCmd := pip.SMembers("events") 70 | pip.Exec() 71 | 72 | room := new(models.Room).Init() 73 | roomRes, _ := roomCmd.Result() 74 | utils.Bind(roomRes, room) 75 | room.ID = roomID 76 | 77 | character := new(models.Character) 78 | characterRes, _ := characterCmd.Result() 79 | utils.Bind(characterRes, character) 80 | character.ID = characterID 81 | 82 | // Load additional room stuff 83 | pip = db.GetInstance().Pipeline() 84 | 85 | characterIDs, _ := roomCharactersCmd.Result() 86 | characterCmds := make([]*redis.StringStringMapCmd, len(characterIDs)) 87 | 88 | for i, characterID := range characterIDs { 89 | characterCmds[i] = pip.HGetAll("character:" + characterID) 90 | } 91 | 92 | elementIDs, _ := roomElementsCmd.Result() 93 | elementCmds := make([]*redis.StringStringMapCmd, len(elementIDs)) 94 | 95 | for i, elementID := range elementIDs { 96 | elementCmds[i] = pip.HGetAll("element:" + elementID) 97 | } 98 | 99 | hallwayIDs, _ := roomHallwaysCmd.Result() 100 | hallwayCmds := make([]*redis.StringStringMapCmd, len(hallwayIDs)) 101 | 102 | for i, hallwayID := range hallwayIDs { 103 | hallwayCmds[i] = pip.HGetAll("hallway:" + hallwayID) 104 | } 105 | 106 | eventIDs, _ := eventsCmd.Result() 107 | eventCmds := make([]*redis.StringStringMapCmd, len(eventIDs)) 108 | 109 | for i, eventID := range eventIDs { 110 | eventCmds[i] = pip.HGetAll("event:" + eventID) 111 | } 112 | 113 | projectIDCmds := make([]*redis.StringCmd, len(characterIDs)) 114 | 115 | if strings.HasPrefix(roomID, "arena:") { 116 | for i, characterID := range characterIDs { 117 | projectIDCmds[i] = pip.Get("character:" + characterID + ":project") 118 | } 119 | } 120 | 121 | // Get friends 122 | teammateIDs, _ := teammatesCmd.Result() 123 | friendIDs, _ := friendsCmd.Result() 124 | requestIDs, _ := requestsCmd.Result() 125 | 126 | teammateCmds := make([]*redis.StringStringMapCmd, len(teammateIDs)) 127 | teammateStatusCmds := make([]*redis.StringCmd, len(teammateIDs)) 128 | 129 | friendCmds := make([]*redis.StringStringMapCmd, len(friendIDs)) 130 | friendStatusCmds := make([]*redis.StringCmd, len(friendIDs)) 131 | 132 | requestCmds := make([]*redis.StringStringMapCmd, len(requestIDs)) 133 | 134 | for i, id := range teammateIDs { 135 | teammateCmds[i] = pip.HGetAll("character:" + id) 136 | teammateStatusCmds[i] = pip.Get("character:" + id + ":active") 137 | } 138 | 139 | for i, id := range friendIDs { 140 | friendCmds[i] = pip.HGetAll("character:" + id) 141 | friendStatusCmds[i] = pip.Get("character:" + id + ":active") 142 | } 143 | 144 | for i, id := range requestIDs { 145 | requestCmds[i] = pip.HGetAll("character:" + id) 146 | } 147 | 148 | var sponsorCmd *redis.StringStringMapCmd 149 | 150 | if len(room.SponsorID) > 0 { 151 | sponsorCmd = pip.HGetAll("sponsor:" + room.SponsorID) 152 | } 153 | 154 | pip.Exec() 155 | 156 | for i, characterCmd := range characterCmds { 157 | characterRes, _ := characterCmd.Result() 158 | room.Characters[characterIDs[i]] = new(models.Character) 159 | utils.Bind(characterRes, room.Characters[characterIDs[i]]) 160 | room.Characters[characterIDs[i]].ID = characterIDs[i] 161 | room.Characters[characterIDs[i]].Email = "" 162 | } 163 | 164 | for i, elementCmd := range elementCmds { 165 | elementRes, _ := elementCmd.Result() 166 | room.Elements = append(room.Elements, new(models.Element)) 167 | utils.Bind(elementRes, room.Elements[i]) 168 | room.Elements[i].ID = elementIDs[i] 169 | } 170 | 171 | for i, hallwayCmd := range hallwayCmds { 172 | hallwayRes, _ := hallwayCmd.Result() 173 | room.Hallways[hallwayIDs[i]] = new(models.Hallway) 174 | utils.Bind(hallwayRes, room.Hallways[hallwayIDs[i]]) 175 | } 176 | 177 | if len(room.SponsorID) > 0 { 178 | sponsorRes, _ := sponsorCmd.Result() 179 | sponsor := new(models.Sponsor) 180 | utils.Bind(sponsorRes, sponsor) 181 | sponsor.ID = room.SponsorID 182 | room.Sponsor = sponsor 183 | } 184 | 185 | // Set data and return 186 | p := new(InitPacket) 187 | p.BasePacket = BasePacket{Type: "init"} 188 | p.Character = character 189 | 190 | feedbackOpen := time.Unix(config.GetConfig().GetInt64("feedback_open"), 0) 191 | 192 | if !character.FeedbackOpened && time.Now().After(feedbackOpen) { 193 | p.OpenFeedback = true 194 | db.GetInstance().HSet("character:"+characterID, "feedbackOpened", true) 195 | } 196 | 197 | p.Room = room 198 | 199 | // Set friends 200 | i := 0 201 | p.Friends = make([]Friend, len(teammateIDs)+len(friendIDs)+len(requestIDs)) 202 | 203 | for j, cmd := range teammateCmds { 204 | data, _ := cmd.Result() 205 | res := new(models.Character) 206 | utils.Bind(data, res) 207 | 208 | active, _ := teammateStatusCmds[j].Result() 209 | status := 2 210 | 211 | if active == "true" { 212 | status = 0 213 | } 214 | 215 | p.Friends[i] = Friend{ 216 | ID: teammateIDs[j], 217 | Name: res.Name, 218 | School: res.School, 219 | Status: status, 220 | Teammate: true, 221 | LastSeen: time.Now(), 222 | } 223 | 224 | i++ 225 | } 226 | 227 | for j, cmd := range friendCmds { 228 | data, _ := cmd.Result() 229 | res := new(models.Character) 230 | utils.Bind(data, res) 231 | 232 | active, _ := friendStatusCmds[j].Result() 233 | status := 2 234 | 235 | if active == "true" { 236 | status = 0 237 | } 238 | 239 | p.Friends[i] = Friend{ 240 | ID: friendIDs[j], 241 | Name: res.Name, 242 | School: res.School, 243 | Status: status, 244 | LastSeen: time.Now(), 245 | } 246 | 247 | i++ 248 | } 249 | 250 | for j, cmd := range requestCmds { 251 | data, _ := cmd.Result() 252 | res := new(models.Character) 253 | utils.Bind(data, res) 254 | 255 | p.Friends[i] = Friend{ 256 | ID: requestIDs[j], 257 | Name: res.Name, 258 | School: res.School, 259 | Status: 2, 260 | Pending: true, 261 | LastSeen: time.Now(), 262 | } 263 | 264 | i++ 265 | } 266 | 267 | p.Events = make([]*models.Event, len(eventIDs)) 268 | for i, eventCmd := range eventCmds { 269 | eventRes, _ := eventCmd.Result() 270 | p.Events[i] = new(models.Event) 271 | utils.Bind(eventRes, p.Events[i]) 272 | } 273 | 274 | if strings.HasPrefix(roomID, "arena:") { 275 | // Load projects 276 | pip = db.GetInstance().Pipeline() 277 | projectCmds := make(map[string]*redis.StringStringMapCmd) 278 | 279 | for _, cmd := range projectIDCmds { 280 | projectID, err := cmd.Result() 281 | 282 | if err != nil || len(projectID) == 0 { 283 | continue 284 | } 285 | 286 | if _, ok := projectCmds[projectID]; ok { 287 | continue 288 | } 289 | 290 | projectCmds[projectID] = pip.HGetAll("project:" + projectID) 291 | } 292 | 293 | pip.Exec() 294 | 295 | p.Projects = make([]*models.Project, len(projectCmds)) 296 | i := 0 297 | 298 | for _, cmd := range projectCmds { 299 | projectRes, _ := cmd.Result() 300 | p.Projects[i] = new(models.Project) 301 | utils.Bind(projectRes, p.Projects[i]) 302 | i++ 303 | } 304 | } 305 | 306 | if needsToken { 307 | // Generate a JWT 308 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ 309 | "id": characterID, 310 | }) 311 | 312 | tokenString, _ := token.SignedString([]byte(config.GetSecret("JWT_SECRET"))) 313 | p.Token = tokenString 314 | } 315 | 316 | // Find all of the possible paths 317 | // TODO: Cache these 318 | p.ElementNames = []string{} 319 | // sess := session.Must(session.NewSession()) 320 | // svc := s3.New(sess) 321 | 322 | // input := &s3.ListObjectsV2Input{ 323 | // Bucket: aws.String("hackmit-playground-2020"), 324 | // Prefix: aws.String("elements/"), 325 | // } 326 | 327 | // result, err := svc.ListObjectsV2(input) 328 | 329 | // if err != nil { 330 | // p.ElementNames = []string{} 331 | // } else { 332 | // elementNames := make([]string, len(result.Contents)-1) 333 | 334 | // for i, item := range result.Contents { 335 | // if i == 0 { 336 | // // First key is the elements directory 337 | // continue 338 | // } 339 | 340 | // elementNames[i-1] = (*item.Key)[9:] 341 | // } 342 | 343 | // p.ElementNames = elementNames 344 | // } 345 | 346 | // Get all room names 347 | p.RoomNames, _ = db.GetInstance().SMembers("rooms").Result() 348 | 349 | // Get settings 350 | p.Settings = new(models.Settings) 351 | settingsRes, _ := settingsCmd.Result() 352 | utils.Bind(settingsRes, p.Settings) 353 | 354 | // Get project 355 | projectID, err := projectIDCmd.Result() 356 | 357 | if err == nil && len(projectID) > 0 { 358 | projectRes, _ := db.GetInstance().HGetAll("project:" + projectID).Result() 359 | p.Character.Project = new(models.Project) 360 | utils.Bind(projectRes, p.Character.Project) 361 | } 362 | 363 | return p 364 | } 365 | 366 | func (p InitPacket) MarshalBinary() ([]byte, error) { 367 | return json.Marshal(p) 368 | } 369 | 370 | func (p InitPacket) UnmarshalBinary(data []byte) error { 371 | return json.Unmarshal(data, p) 372 | } 373 | -------------------------------------------------------------------------------- /socket/packet/join.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | 7 | "github.com/techx/playground/db" 8 | "github.com/techx/playground/db/models" 9 | "github.com/techx/playground/utils" 10 | ) 11 | 12 | // Sent by clients after receiving the init packet. Identifies them to the 13 | // server, and in turn other clients 14 | type JoinPacket struct { 15 | BasePacket 16 | Packet `json:",omitempty"` 17 | 18 | // Client attributes 19 | Name string `json:"name,omitempty"` 20 | QuillToken string `json:"quillToken,omitempty"` 21 | Token string `json:"token,omitempty"` 22 | 23 | Email string `json:"email,omitempty"` 24 | Code int `json:"code,omitempty"` 25 | 26 | // Server attributes 27 | Character *models.Character `json:"character"` 28 | ClientID string `json:"clientId,omitempty"` 29 | Project *models.Project `json:"project"` 30 | Room string `json:"room"` 31 | } 32 | 33 | func NewJoinPacket(character *models.Character, room string) *JoinPacket { 34 | p := new(JoinPacket) 35 | p.BasePacket = BasePacket{Type: "join"} 36 | p.Character = character 37 | p.Character.Email = "" 38 | 39 | if strings.HasPrefix(p.Room, "arena:") { 40 | p.SetProject() 41 | } 42 | 43 | return p 44 | } 45 | 46 | func (p *JoinPacket) SetProject() { 47 | projectID, err := db.GetInstance().Get("character:" + p.Character.ID + ":project").Result() 48 | 49 | if err != nil || len(projectID) == 0 { 50 | return 51 | } 52 | 53 | p.Project = new(models.Project) 54 | projectRes, _ := db.GetInstance().HGetAll("project:" + projectID).Result() 55 | utils.Bind(projectRes, p.Project) 56 | } 57 | 58 | func (p JoinPacket) PermissionCheck(characterID string, role models.Role) bool { 59 | return true 60 | } 61 | 62 | func (p JoinPacket) MarshalBinary() ([]byte, error) { 63 | return json.Marshal(p) 64 | } 65 | 66 | func (p JoinPacket) UnmarshalBinary(data []byte) error { 67 | return json.Unmarshal(data, p) 68 | } 69 | -------------------------------------------------------------------------------- /socket/packet/jukebox_warning.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by ingests when a user opens the jukebox for the first time 10 | type JukeboxWarningPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | } 14 | 15 | func NewJukeboxWarningPacket() *JukeboxWarningPacket { 16 | return &JukeboxWarningPacket{ 17 | BasePacket: BasePacket{ 18 | Type: "jukebox_warning", 19 | }, 20 | } 21 | } 22 | 23 | func (p *JukeboxWarningPacket) Init() *JukeboxWarningPacket { 24 | p.BasePacket = BasePacket{Type: "jukebox_warning"} 25 | return p 26 | } 27 | 28 | func (p JukeboxWarningPacket) PermissionCheck(characterID string, role models.Role) bool { 29 | return len(characterID) > 0 30 | } 31 | 32 | func (p JukeboxWarningPacket) MarshalBinary() ([]byte, error) { 33 | return json.Marshal(p) 34 | } 35 | 36 | func (p JukeboxWarningPacket) UnmarshalBinary(data []byte) error { 37 | return json.Unmarshal(data, p) 38 | } 39 | -------------------------------------------------------------------------------- /socket/packet/leave.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by ingests when a client disconnects 10 | type LeavePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The character who is leaving 15 | Character *models.Character `json:"character"` 16 | 17 | // The room that the client is leaving 18 | Room string `json:"room"` 19 | } 20 | 21 | func NewLeavePacket(character *models.Character, room string) *LeavePacket { 22 | p := new(LeavePacket) 23 | p.BasePacket = BasePacket{Type: "leave"} 24 | p.Character = character 25 | p.Room = room 26 | return p 27 | } 28 | 29 | func (p LeavePacket) PermissionCheck(characterID string, role models.Role) bool { 30 | return len(characterID) > 0 31 | } 32 | 33 | func (p LeavePacket) MarshalBinary() ([]byte, error) { 34 | return json.Marshal(p) 35 | } 36 | 37 | func (p LeavePacket) UnmarshalBinary(data []byte) error { 38 | return json.Unmarshal(data, p) 39 | } 40 | -------------------------------------------------------------------------------- /socket/packet/map.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/go-redis/redis/v7" 7 | "github.com/techx/playground/db" 8 | "github.com/techx/playground/db/models" 9 | "github.com/techx/playground/utils" 10 | ) 11 | 12 | type MapPacket struct { 13 | BasePacket 14 | Packet `json:",omitempty"` 15 | 16 | Locations []*models.Location `json:"locations"` 17 | } 18 | 19 | func NewMapPacket() *MapPacket { 20 | // Load locations from Redis 21 | locationIDs, _ := db.GetInstance().SMembers("locations").Result() 22 | 23 | pip := db.GetInstance().Pipeline() 24 | locationCmds := make([]*redis.StringStringMapCmd, len(locationIDs)) 25 | 26 | for i, locationID := range locationIDs { 27 | locationCmds[i] = pip.HGetAll("location:" + locationID) 28 | } 29 | 30 | pip.Exec() 31 | locations := make([]*models.Location, len(locationCmds)) 32 | 33 | for i, locationCmd := range locationCmds { 34 | locationRes, _ := locationCmd.Result() 35 | locations[i] = new(models.Location) 36 | utils.Bind(locationRes, locations[i]) 37 | } 38 | 39 | // Send locations back to client 40 | return &MapPacket{ 41 | BasePacket: BasePacket{ 42 | Type: "map", 43 | }, 44 | Locations: locations, 45 | } 46 | } 47 | 48 | func (p MapPacket) MarshalBinary() ([]byte, error) { 49 | return json.Marshal(p) 50 | } 51 | 52 | func (p MapPacket) UnmarshalBinary(data []byte) error { 53 | return json.Unmarshal(data, p) 54 | } 55 | -------------------------------------------------------------------------------- /socket/packet/message.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type MessagePacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | *models.Message 13 | } 14 | 15 | func (p MessagePacket) PermissionCheck(characterID string, role models.Role) bool { 16 | return len(characterID) > 0 17 | } 18 | 19 | func (p MessagePacket) MarshalBinary() ([]byte, error) { 20 | return json.Marshal(p) 21 | } 22 | 23 | func (p MessagePacket) UnmarshalBinary(data []byte) error { 24 | return json.Unmarshal(data, p) 25 | } 26 | -------------------------------------------------------------------------------- /socket/packet/messages.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type MessagesPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | Messages []*models.Message `json:"messages"` 14 | Recipient string `json:"recipient"` 15 | } 16 | 17 | func NewMessagesPacket(messages []*models.Message, recipient string) *MessagesPacket { 18 | return &MessagesPacket{ 19 | BasePacket: BasePacket{ 20 | Type: "messages", 21 | }, 22 | Messages: messages, 23 | Recipient: recipient, 24 | } 25 | } 26 | 27 | func (p MessagesPacket) MarshalBinary() ([]byte, error) { 28 | return json.Marshal(p) 29 | } 30 | 31 | func (p MessagesPacket) UnmarshalBinary(data []byte) error { 32 | return json.Unmarshal(data, p) 33 | } 34 | -------------------------------------------------------------------------------- /socket/packet/move.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they move around 10 | type MovePacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The id of the client who is moving 15 | ID string `json:"id"` 16 | 17 | // The room that the client is moving in 18 | Room string `json:"room"` 19 | 20 | // The client's new x position (0-1) 21 | X float64 `json:"x"` 22 | 23 | // The client's new y position (0-1) 24 | Y float64 `json:"y"` 25 | } 26 | 27 | func NewMovePacket(id, room string, x, y float64) *MovePacket { 28 | return &MovePacket{ 29 | BasePacket: BasePacket{ 30 | Type: "move", 31 | }, 32 | ID: id, 33 | Room: room, 34 | X: x, 35 | Y: y, 36 | } 37 | } 38 | 39 | func (p MovePacket) PermissionCheck(characterID string, role models.Role) bool { 40 | return len(characterID) > 0 41 | } 42 | 43 | func (p MovePacket) MarshalBinary() ([]byte, error) { 44 | return json.Marshal(p) 45 | } 46 | 47 | func (p MovePacket) UnmarshalBinary(data []byte) error { 48 | return json.Unmarshal(data, p) 49 | } 50 | -------------------------------------------------------------------------------- /socket/packet/notification.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type NotificationLevel int 8 | 9 | const ( 10 | // Low-level/info notification -- we'll only display the notification pop-up in Hack Penguin 11 | Low NotificationLevel = iota 12 | 13 | // Medium-level notification -- we'll also contact them through their preferred methods of notification 14 | Medium 15 | 16 | // Urgent notification -- we will reach out through every method we have available 17 | High 18 | ) 19 | 20 | type NotificationType string 21 | 22 | const ( 23 | Achievement NotificationType = "achievement" 24 | Message = "message" 25 | ) 26 | 27 | type NotificationPacket struct { 28 | BasePacket 29 | Packet `json:",omitempty"` 30 | 31 | NotificationType NotificationType `json:"notificationType"` 32 | Data map[string]interface{} `json:"data"` 33 | Level NotificationLevel `json:"level"` 34 | } 35 | 36 | func newNotificationPacket(notificationType NotificationType, level NotificationLevel, data map[string]interface{}) *NotificationPacket { 37 | return &NotificationPacket{ 38 | BasePacket: BasePacket{ 39 | Type: "notification", 40 | }, 41 | NotificationType: notificationType, 42 | Data: data, 43 | Level: level, 44 | } 45 | } 46 | 47 | func NewAchievementNotificationPacket(id string) *NotificationPacket { 48 | return newNotificationPacket(Achievement, Low, map[string]interface{}{ 49 | "id": id, 50 | }) 51 | } 52 | 53 | func NewMessageNotificationPacket(text string) *NotificationPacket { 54 | return newNotificationPacket(Message, Low, map[string]interface{}{ 55 | "text": text, 56 | }) 57 | } 58 | 59 | func (p NotificationPacket) MarshalBinary() ([]byte, error) { 60 | return json.Marshal(p) 61 | } 62 | 63 | func (p NotificationPacket) UnmarshalBinary(data []byte) error { 64 | return json.Unmarshal(data, p) 65 | } 66 | -------------------------------------------------------------------------------- /socket/packet/parse.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | func ParsePacket(data []byte) (Packet, error) { 9 | res := BasePacket{} 10 | 11 | if err := json.Unmarshal(data, &res); err != nil { 12 | return nil, err 13 | } 14 | 15 | switch res.Type { 16 | case "add_email": 17 | p := AddEmailPacket{} 18 | json.Unmarshal(data, &p) 19 | return p, nil 20 | case "auth", "join": 21 | p := JoinPacket{} 22 | json.Unmarshal(data, &p) 23 | return p, nil 24 | case "chat": 25 | p := ChatPacket{} 26 | json.Unmarshal(data, &p) 27 | return p, nil 28 | case "dance": 29 | p := DancePacket{} 30 | json.Unmarshal(data, &p) 31 | return p, nil 32 | case "element_toggle": 33 | p := ElementTogglePacket{} 34 | json.Unmarshal(data, &p) 35 | return p, nil 36 | case "element_update": 37 | p := ElementUpdatePacket{} 38 | json.Unmarshal(data, &p) 39 | return p, nil 40 | case "email_code": 41 | p := EmailCodePacket{} 42 | json.Unmarshal(data, &p) 43 | return p, nil 44 | case "event": 45 | p := EventPacket{} 46 | json.Unmarshal(data, &p) 47 | return p, nil 48 | case "friend_request": 49 | p := FriendRequestPacket{} 50 | json.Unmarshal(data, &p) 51 | return p, nil 52 | case "friend_update": 53 | p := FriendUpdatePacket{} 54 | json.Unmarshal(data, &p) 55 | return p, nil 56 | case "get_achievements": 57 | p := GetAchievementsPacket{} 58 | json.Unmarshal(data, &p) 59 | return p, nil 60 | case "get_map": 61 | p := GetMapPacket{} 62 | json.Unmarshal(data, &p) 63 | return p, nil 64 | case "get_messages": 65 | p := GetMessagesPacket{} 66 | json.Unmarshal(data, &p) 67 | return p, nil 68 | case "get_current_song": 69 | p := GetCurrentSongPacket{} 70 | json.Unmarshal(data, &p) 71 | return p, nil 72 | case "get_songs": 73 | p := GetSongsPacket{} 74 | json.Unmarshal(data, &p) 75 | return p, nil 76 | case "get_sponsor": 77 | p := GetSponsorPacket{} 78 | json.Unmarshal(data, &p) 79 | return p, nil 80 | case "hallway_add": 81 | p := HallwayAddPacket{} 82 | json.Unmarshal(data, &p) 83 | return p, nil 84 | case "hallway_delete": 85 | p := HallwayDeletePacket{} 86 | json.Unmarshal(data, &p) 87 | return p, nil 88 | case "hallway_update": 89 | p := HallwayUpdatePacket{} 90 | json.Unmarshal(data, &p) 91 | return p, nil 92 | case "jukebox_warning": 93 | p := JukeboxWarningPacket{} 94 | json.Unmarshal(data, &p) 95 | return p, nil 96 | case "leave": 97 | p := LeavePacket{} 98 | json.Unmarshal(data, &p) 99 | return p, nil 100 | case "message": 101 | p := MessagePacket{} 102 | json.Unmarshal(data, &p) 103 | return p, nil 104 | case "move": 105 | p := MovePacket{} 106 | json.Unmarshal(data, &p) 107 | return p, nil 108 | case "play_song": 109 | p := PlaySongPacket{} 110 | json.Unmarshal(data, &p) 111 | return p, nil 112 | case "project_form": 113 | p := ProjectFormPacket{} 114 | json.Unmarshal(data, &p) 115 | return p, nil 116 | case "queue_join": 117 | p := QueueJoinPacket{} 118 | json.Unmarshal(data, &p) 119 | return p, nil 120 | case "queue_remove": 121 | p := QueueRemovePacket{} 122 | json.Unmarshal(data, &p) 123 | return p, nil 124 | case "queue_subscribe": 125 | p := QueueSubscribePacket{} 126 | json.Unmarshal(data, &p) 127 | return p, nil 128 | case "queue_unsubscribe": 129 | p := QueueUnsubscribePacket{} 130 | json.Unmarshal(data, &p) 131 | return p, nil 132 | case "queue_update_hacker": 133 | p := QueueUpdateHackerPacket{} 134 | json.Unmarshal(data, &p) 135 | return p, nil 136 | case "queue_update_sponsor": 137 | p := QueueUpdateSponsorPacket{} 138 | json.Unmarshal(data, &p) 139 | return p, nil 140 | case "register": 141 | p := RegisterPacket{} 142 | json.Unmarshal(data, &p) 143 | return p, nil 144 | case "report": 145 | p := ReportPacket{} 146 | json.Unmarshal(data, &p) 147 | return p, nil 148 | case "room_add": 149 | p := RoomAddPacket{} 150 | json.Unmarshal(data, &p) 151 | return p, nil 152 | case "settings": 153 | p := SettingsPacket{} 154 | json.Unmarshal(data, &p) 155 | return p, nil 156 | case "song": 157 | p := SongPacket{} 158 | json.Unmarshal(data, &p) 159 | return p, nil 160 | case "status": 161 | p := StatusPacket{} 162 | json.Unmarshal(data, &p) 163 | return p, nil 164 | case "teleport", "teleport_home": 165 | p := TeleportPacket{} 166 | json.Unmarshal(data, &p) 167 | return p, nil 168 | case "update_map": 169 | p := UpdateMapPacket{} 170 | json.Unmarshal(data, &p) 171 | return p, nil 172 | case "update_sponsor": 173 | p := UpdateSponsorPacket{} 174 | json.Unmarshal(data, &p) 175 | return p, nil 176 | case "wardrobe_change": 177 | p := WardrobeChangePacket{} 178 | json.Unmarshal(data, &p) 179 | return p, nil 180 | default: 181 | return nil, errors.New("Invalid packet type: " + res.Type) 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /socket/packet/play_song.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by ingests when a song is added to queue 10 | type PlaySongPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | Song *models.Song `json:"song"` 14 | Start int `json:"start"` 15 | End int `json:"end"` 16 | } 17 | 18 | func NewPlaySongPacket(song *models.Song, start int) *PlaySongPacket { 19 | return &PlaySongPacket{ 20 | BasePacket: BasePacket{ 21 | Type: "play_song", 22 | }, 23 | Song: song, 24 | Start: start, 25 | } 26 | } 27 | 28 | func (p *PlaySongPacket) Init(song *models.Song) *PlaySongPacket { 29 | p.BasePacket = BasePacket{Type: "play_song"} 30 | p.Song = song 31 | return p 32 | } 33 | 34 | func (p PlaySongPacket) PermissionCheck(characterID string, role models.Role) bool { 35 | return len(characterID) > 0 36 | } 37 | 38 | func (p PlaySongPacket) MarshalBinary() ([]byte, error) { 39 | return json.Marshal(p) 40 | } 41 | 42 | func (p PlaySongPacket) UnmarshalBinary(data []byte) error { 43 | return json.Unmarshal(data, p) 44 | } 45 | -------------------------------------------------------------------------------- /socket/packet/project_form.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type ProjectFormPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | Challenges []string `json:"challenges"` 14 | Teammates []string `json:"teammates"` 15 | *models.Project 16 | } 17 | 18 | func (p ProjectFormPacket) PermissionCheck(characterID string, role models.Role) bool { 19 | return len(characterID) > 0 20 | } 21 | 22 | func (p ProjectFormPacket) MarshalBinary() ([]byte, error) { 23 | return json.Marshal(p) 24 | } 25 | 26 | func (p ProjectFormPacket) UnmarshalBinary(data []byte) error { 27 | return json.Unmarshal(data, p) 28 | } 29 | -------------------------------------------------------------------------------- /socket/packet/queue_join.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by hackers to join a sponsor's queue 10 | type QueueJoinPacket struct { 11 | BasePacket 12 | 13 | SponsorID string `json:"sponsorId"` 14 | Interests []string `json:"interests"` 15 | } 16 | 17 | func (p QueueJoinPacket) PermissionCheck(characterID string, role models.Role) bool { 18 | return len(characterID) > 0 && (role == models.Hacker || role == models.Organizer) 19 | } 20 | 21 | func (p QueueJoinPacket) MarshalBinary() ([]byte, error) { 22 | return json.Marshal(p) 23 | } 24 | 25 | func (p QueueJoinPacket) UnmarshalBinary(data []byte) error { 26 | return json.Unmarshal(data, p) 27 | } 28 | -------------------------------------------------------------------------------- /socket/packet/queue_remove.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by hackers to take themselves off queue 10 | type QueueRemovePacket struct { 11 | BasePacket 12 | 13 | SponsorID string `json:"sponsorId"` 14 | CharacterID string `json:"characterId"` 15 | Zoom string `json:"zoom"` 16 | } 17 | 18 | func (p QueueRemovePacket) PermissionCheck(characterID string, role models.Role) bool { 19 | if len(characterID) == 0 { 20 | // User is not signed in 21 | return false 22 | } 23 | 24 | // Can remove if the hacker is removing themself, or if the sponsor is taking them off the queue 25 | return role == models.SponsorRep || characterID == p.CharacterID 26 | } 27 | 28 | func (p QueueRemovePacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p QueueRemovePacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/queue_subscribe.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // sent by hackers and sponsors to subscribe to queue updates 10 | type QueueSubscribePacket struct { 11 | BasePacket 12 | 13 | SponsorID string `json:"sponsorId"` 14 | 15 | Characters []*models.Character `json:"characters"` 16 | } 17 | 18 | func (p QueueSubscribePacket) PermissionCheck(characterID string, role models.Role) bool { 19 | return len(characterID) > 0 20 | } 21 | 22 | func (p QueueSubscribePacket) MarshalBinary() ([]byte, error) { 23 | return json.Marshal(p) 24 | } 25 | 26 | func (p QueueSubscribePacket) UnmarshalBinary(data []byte) error { 27 | return json.Unmarshal(data, p) 28 | } 29 | -------------------------------------------------------------------------------- /socket/packet/queue_unsubscribe.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // sent by hackers and sponsors to unsubscribe to queue updates 10 | type QueueUnsubscribePacket struct { 11 | BasePacket 12 | 13 | SponsorID string `json:"sponsorId"` 14 | } 15 | 16 | func (p QueueUnsubscribePacket) PermissionCheck(characterID string, role models.Role) bool { 17 | return len(characterID) > 0 18 | } 19 | 20 | func (p QueueUnsubscribePacket) MarshalBinary() ([]byte, error) { 21 | return json.Marshal(p) 22 | } 23 | 24 | func (p QueueUnsubscribePacket) UnmarshalBinary(data []byte) error { 25 | return json.Unmarshal(data, p) 26 | } 27 | -------------------------------------------------------------------------------- /socket/packet/queue_update_hacker.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by server to update a hacker on their position in the queue 10 | type QueueUpdateHackerPacket struct { 11 | BasePacket 12 | 13 | SponsorID string `json:"sponsorId"` 14 | Position int `json:"position"` 15 | URL string `json:"url,omitempty"` 16 | 17 | // Server attributes 18 | CharacterIDs []string `json:"characterIds"` 19 | } 20 | 21 | func NewQueueUpdateHackerPacket(sponsorID string, position int, url string) *QueueUpdateHackerPacket { 22 | return &QueueUpdateHackerPacket{ 23 | BasePacket: BasePacket{ 24 | Type: "queue_update_hacker", 25 | }, 26 | SponsorID: sponsorID, 27 | Position: position, 28 | URL: url, 29 | } 30 | } 31 | 32 | // This isn't needed -- remove later 33 | func (p QueueUpdateHackerPacket) PermissionCheck(characterID string, role models.Role) bool { 34 | return len(characterID) > 0 35 | } 36 | 37 | func (p QueueUpdateHackerPacket) MarshalBinary() ([]byte, error) { 38 | return json.Marshal(p) 39 | } 40 | 41 | func (p QueueUpdateHackerPacket) UnmarshalBinary(data []byte) error { 42 | return json.Unmarshal(data, p) 43 | } 44 | -------------------------------------------------------------------------------- /socket/packet/queue_update_sponsor.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by server to update a hacker on their position in the queue 10 | type QueueUpdateSponsorPacket struct { 11 | BasePacket 12 | 13 | Subscribers []*models.QueueSubscriber `json:"subscribers"` 14 | 15 | // Server attributes 16 | CharacterIDs []string `json:"characterIds"` 17 | } 18 | 19 | func NewQueueUpdateSponsorPacket(subscribers []*models.QueueSubscriber) *QueueUpdateSponsorPacket { 20 | return &QueueUpdateSponsorPacket{ 21 | BasePacket: BasePacket{ 22 | Type: "queue_update_sponsor", 23 | }, 24 | Subscribers: subscribers, 25 | } 26 | } 27 | 28 | // This isn't needed -- remove later 29 | func (p QueueUpdateSponsorPacket) PermissionCheck(characterID string, role models.Role) bool { 30 | return len(characterID) > 0 31 | } 32 | 33 | func (p QueueUpdateSponsorPacket) MarshalBinary() ([]byte, error) { 34 | return json.Marshal(p) 35 | } 36 | 37 | func (p QueueUpdateSponsorPacket) UnmarshalBinary(data []byte) error { 38 | return json.Unmarshal(data, p) 39 | } 40 | -------------------------------------------------------------------------------- /socket/packet/register.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | webpush "github.com/SherClockHolmes/webpush-go" 7 | "github.com/techx/playground/db/models" 8 | ) 9 | 10 | type RegisterPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | Name string `json:"name"` 15 | Location string `json:"location"` 16 | Bio string `json:"bio"` 17 | PhoneNumber string `json:"phoneNumber"` 18 | BrowserSubscription *webpush.Subscription `json:"browserSubscription"` 19 | } 20 | 21 | func (p RegisterPacket) PermissionCheck(characterID string, role models.Role) bool { 22 | return true 23 | } 24 | 25 | func (p RegisterPacket) MarshalBinary() ([]byte, error) { 26 | return json.Marshal(p) 27 | } 28 | 29 | func (p RegisterPacket) UnmarshalBinary(data []byte) error { 30 | return json.Unmarshal(data, p) 31 | } 32 | -------------------------------------------------------------------------------- /socket/packet/report.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type ReportPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | CharacterID string `json:"characterId"` 14 | Text string `json:"text"` 15 | } 16 | 17 | func (p ReportPacket) PermissionCheck(characterID string, role models.Role) bool { 18 | return len(characterID) > 0 19 | } 20 | 21 | func (p ReportPacket) MarshalBinary() ([]byte, error) { 22 | return json.Marshal(p) 23 | } 24 | 25 | func (p ReportPacket) UnmarshalBinary(data []byte) error { 26 | return json.Unmarshal(data, p) 27 | } 28 | -------------------------------------------------------------------------------- /socket/packet/room_add.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when they're adding an element 10 | type RoomAddPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The name of the new room 15 | ID string `json:"id"` 16 | 17 | // The background path for this room 18 | Background string `json:"background"` 19 | 20 | // True if this is a sponsor room 21 | Sponsor bool `json:"sponsor"` 22 | } 23 | 24 | func (p RoomAddPacket) PermissionCheck(characterID string, role models.Role) bool { 25 | return role == models.Organizer 26 | } 27 | 28 | func (p RoomAddPacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p RoomAddPacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/settings.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when settings are changed 10 | type SettingsPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The client's new settings 15 | Settings *models.Settings `json:"settings"` 16 | 17 | CheckTwitter bool `json:"checkTwitter"` 18 | 19 | Location string `json:"location"` 20 | Bio string `json:"bio"` 21 | Zoom string `json:"zoom"` 22 | } 23 | 24 | func (p SettingsPacket) PermissionCheck(characterID string, role models.Role) bool { 25 | return len(characterID) > 0 26 | } 27 | 28 | func (p SettingsPacket) MarshalBinary() ([]byte, error) { 29 | return json.Marshal(p) 30 | } 31 | 32 | func (p SettingsPacket) UnmarshalBinary(data []byte) error { 33 | return json.Unmarshal(data, p) 34 | } 35 | -------------------------------------------------------------------------------- /socket/packet/song.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by ingests when a song is added to queue 10 | type SongPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | *models.Song 14 | RequiresWarning bool `json:"requiresWarning"` 15 | Remove bool `json:"remove"` 16 | } 17 | 18 | func (p *SongPacket) Init(song *models.Song) *SongPacket { 19 | p.BasePacket = BasePacket{Type: "song"} 20 | p.Song = song 21 | p.RequiresWarning = false 22 | p.Remove = false 23 | return p 24 | } 25 | 26 | func (p SongPacket) PermissionCheck(characterID string, role models.Role) bool { 27 | return len(characterID) > 0 && (!p.Remove || role == models.Organizer) 28 | } 29 | 30 | func (p SongPacket) MarshalBinary() ([]byte, error) { 31 | return json.Marshal(p) 32 | } 33 | 34 | func (p SongPacket) UnmarshalBinary(data []byte) error { 35 | return json.Unmarshal(data, p) 36 | } 37 | -------------------------------------------------------------------------------- /socket/packet/songs.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type SongsPacket struct { 10 | BasePacket 11 | 12 | Songs []*models.Song `json:"songs"` 13 | } 14 | 15 | func NewSongsPacket(songs []*models.Song) *SongsPacket { 16 | return &SongsPacket{ 17 | BasePacket: BasePacket{ 18 | Type: "songs", 19 | }, 20 | Songs: songs, 21 | } 22 | } 23 | 24 | func (p SongsPacket) MarshalBinary() ([]byte, error) { 25 | return json.Marshal(p) 26 | } 27 | 28 | func (p SongsPacket) UnmarshalBinary(data []byte) error { 29 | return json.Unmarshal(data, p) 30 | } -------------------------------------------------------------------------------- /socket/packet/sponsor.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db" 7 | "github.com/techx/playground/db/models" 8 | "github.com/techx/playground/utils" 9 | ) 10 | 11 | type SponsorPacket struct { 12 | BasePacket 13 | Packet `json:",omitempty"` 14 | 15 | Sponsor *models.Sponsor `json:"sponsor"` 16 | } 17 | 18 | func NewSponsorPacket(sponsorID string) *SponsorPacket { 19 | var sponsor models.Sponsor 20 | sponsorRes, _ := db.GetInstance().HGetAll("sponsor:" + sponsorID).Result() 21 | utils.Bind(sponsorRes, &sponsor) 22 | 23 | return &SponsorPacket{ 24 | BasePacket: BasePacket{ 25 | Type: "sponsor", 26 | }, 27 | Sponsor: &sponsor, 28 | } 29 | } 30 | 31 | func (p SponsorPacket) MarshalBinary() ([]byte, error) { 32 | return json.Marshal(p) 33 | } 34 | 35 | func (p SponsorPacket) UnmarshalBinary(data []byte) error { 36 | return json.Unmarshal(data, p) 37 | } 38 | -------------------------------------------------------------------------------- /socket/packet/status.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by clients when the window gains or loses focus 10 | type StatusPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // True if the user is online and has the tab open -- false if the window doesn't have focus 15 | Active bool `json:"active"` 16 | 17 | // True if the user has the tab open, regardless of focus 18 | Online bool `json:"online"` 19 | 20 | // The ID of the character who this is a status update for 21 | ID string `json:"id"` 22 | 23 | // Server attributes 24 | FriendIDs []string `json:"friendIds"` 25 | TeammateIDs []string `json:"teammateIds"` 26 | } 27 | 28 | func NewStatusPacket(characterID string, online bool) *StatusPacket { 29 | return &StatusPacket{ 30 | BasePacket: BasePacket{ 31 | Type: "status", 32 | }, 33 | ID: characterID, 34 | Active: online, 35 | Online: online, 36 | } 37 | } 38 | 39 | func (p StatusPacket) PermissionCheck(characterID string, role models.Role) bool { 40 | return len(characterID) > 0 41 | } 42 | 43 | func (p StatusPacket) MarshalBinary() ([]byte, error) { 44 | return json.Marshal(p) 45 | } 46 | 47 | func (p StatusPacket) UnmarshalBinary(data []byte) error { 48 | return json.Unmarshal(data, p) 49 | } 50 | -------------------------------------------------------------------------------- /socket/packet/teleport.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | // Sent by ingests when a client changes rooms 10 | type TeleportPacket struct { 11 | BasePacket 12 | Packet `json:",omitempty"` 13 | 14 | // The charcater who is teleporting 15 | Character *models.Character `json:"character"` 16 | 17 | // The room they're moving from 18 | From string `json:"from"` 19 | 20 | // The room they're moving to 21 | To string `json:"to"` 22 | 23 | // The resulting X coordinate 24 | X float64 `json:"x"` 25 | 26 | // The resulting Y coordinate 27 | Y float64 `json:"y"` 28 | } 29 | 30 | func NewTeleportPacket(character *models.Character, from, to string) *TeleportPacket { 31 | p := new(TeleportPacket) 32 | p.BasePacket = BasePacket{Type: "teleport"} 33 | p.From = from 34 | p.To = to 35 | p.Character = character 36 | p.X = 0.5 37 | p.Y = 0.5 38 | return p 39 | } 40 | 41 | func (p TeleportPacket) PermissionCheck(characterID string, role models.Role) bool { 42 | return len(characterID) > 0 43 | } 44 | 45 | func (p TeleportPacket) MarshalBinary() ([]byte, error) { 46 | return json.Marshal(p) 47 | } 48 | 49 | func (p TeleportPacket) UnmarshalBinary(data []byte) error { 50 | return json.Unmarshal(data, p) 51 | } 52 | -------------------------------------------------------------------------------- /socket/packet/update_map.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type UpdateMapPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | *models.Location `json:"location"` 13 | } 14 | 15 | func (p UpdateMapPacket) PermissionCheck(characterID string, role models.Role) bool { 16 | return len(characterID) > 0 17 | } 18 | 19 | func (p UpdateMapPacket) MarshalBinary() ([]byte, error) { 20 | return json.Marshal(p) 21 | } 22 | 23 | func (p UpdateMapPacket) UnmarshalBinary(data []byte) error { 24 | return json.Unmarshal(data, p) 25 | } 26 | -------------------------------------------------------------------------------- /socket/packet/update_sponsor.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type UpdateSponsorPacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | *models.Sponsor 13 | 14 | SetQueueOpen bool `json:"setQueueOpen"` 15 | } 16 | 17 | func (p UpdateSponsorPacket) PermissionCheck(characterID string, role models.Role) bool { 18 | return len(characterID) > 0 && (role == models.SponsorRep || role == models.Organizer) 19 | } 20 | 21 | func (p UpdateSponsorPacket) MarshalBinary() ([]byte, error) { 22 | return json.Marshal(p) 23 | } 24 | 25 | func (p UpdateSponsorPacket) UnmarshalBinary(data []byte) error { 26 | return json.Unmarshal(data, p) 27 | } 28 | -------------------------------------------------------------------------------- /socket/packet/wardrobe_change.go: -------------------------------------------------------------------------------- 1 | package packet 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/techx/playground/db/models" 7 | ) 8 | 9 | type WardrobeChangePacket struct { 10 | BasePacket 11 | Packet `json:",omitempty"` 12 | 13 | CharacterID string `json:"characterId"` 14 | Room string `json:"room"` 15 | 16 | EyeColor string `json:"eyeColor"` 17 | SkinColor string `json:"skinColor"` 18 | ShirtColor string `json:"shirtColor"` 19 | PantsColor string `json:"pantsColor"` 20 | } 21 | 22 | func (p WardrobeChangePacket) PermissionCheck(characterID string, role models.Role) bool { 23 | if role == models.Organizer { 24 | return true 25 | } 26 | 27 | return len(characterID) > 0 28 | } 29 | 30 | func (p WardrobeChangePacket) MarshalBinary() ([]byte, error) { 31 | return json.Marshal(p) 32 | } 33 | 34 | func (p WardrobeChangePacket) UnmarshalBinary(data []byte) error { 35 | return json.Unmarshal(data, p) 36 | } 37 | -------------------------------------------------------------------------------- /socket/serve.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | const ( 13 | // Time allowed to write a message to the peer. 14 | writeWait = 10 * time.Second 15 | 16 | // Time allowed to read the next pong message from the peer. 17 | pongWait = 60 * time.Second 18 | 19 | // Send pings to peer with this period. Must be less than pongWait. 20 | pingPeriod = (pongWait * 9) / 10 21 | 22 | // Maximum message size allowed from peer. 23 | maxMessageSize = 4096 24 | ) 25 | 26 | var ( 27 | newline = []byte{'\n'} 28 | space = []byte{' '} 29 | ) 30 | 31 | var upgrader = websocket.Upgrader{ 32 | ReadBufferSize: 1024, 33 | WriteBufferSize: 1024, 34 | } 35 | 36 | // readPump pumps messages from the websocket connection to the hub. 37 | // 38 | // The application runs readPump in a per-connection goroutine. The application 39 | // ensures that there is at most one reader on a connection by executing all 40 | // reads from this goroutine. 41 | func (c *Client) readPump() { 42 | defer func() { 43 | c.hub.unregister <- c 44 | c.conn.Close() 45 | }() 46 | c.conn.SetReadLimit(maxMessageSize) 47 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 48 | c.conn.SetPongHandler(func(string) error { 49 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 50 | return nil 51 | }) 52 | for { 53 | _, message, err := c.conn.ReadMessage() 54 | if err != nil { 55 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 56 | log.Printf("error: %v", err) 57 | } 58 | break 59 | } 60 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 61 | 62 | sendMessage := SocketMessage{message, c} 63 | c.hub.broadcast <- &sendMessage 64 | } 65 | } 66 | 67 | // writePump pumps messages from the hub to the websocket connection. 68 | // 69 | // A goroutine running writePump is started for each connection. The 70 | // application ensures that there is at most one writer to a connection by 71 | // executing all writes from this goroutine. 72 | func (c *Client) writePump() { 73 | ticker := time.NewTicker(pingPeriod) 74 | defer func() { 75 | ticker.Stop() 76 | c.conn.Close() 77 | }() 78 | for { 79 | select { 80 | case message, ok := <-c.send: 81 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 82 | if !ok { 83 | // The hub closed the channel. 84 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 85 | return 86 | } 87 | 88 | w, err := c.conn.NextWriter(websocket.TextMessage) 89 | if err != nil { 90 | return 91 | } 92 | w.Write(message) 93 | 94 | // Add queued chat messages to the current websocket message. 95 | n := len(c.send) 96 | for i := 0; i < n; i++ { 97 | w.Write(newline) 98 | w.Write(<-c.send) 99 | } 100 | 101 | if err := w.Close(); err != nil { 102 | return 103 | } 104 | case <-ticker.C: 105 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 106 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 107 | return 108 | } 109 | } 110 | } 111 | } 112 | 113 | // ServeWs handles websocket requests from the peer. 114 | func ServeWs(hub *Hub, w http.ResponseWriter, r *http.Request) { 115 | // TODO: Create more strict origin checks -- this is a security risk 116 | upgrader.CheckOrigin = func(r *http.Request) bool { 117 | return true 118 | } 119 | 120 | // Creates a websocket connection from the HTTP connection 121 | conn, err := upgrader.Upgrade(w, r, nil) 122 | 123 | if err != nil { 124 | log.Println("ERROR: Unable to upgrade connection to WS") 125 | return 126 | } 127 | 128 | // Create a new client with a unique ID 129 | client := NewClient(hub, conn) 130 | client.hub.register <- client 131 | 132 | // Allow collection of memory referenced by the caller by doing all work in 133 | // new goroutines 134 | go client.writePump() 135 | go client.readPump() 136 | } 137 | -------------------------------------------------------------------------------- /utils/bind.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "reflect" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func StructToMap(item interface{}) map[string]interface{} { 13 | out := make(map[string]interface{}) 14 | 15 | v := reflect.ValueOf(item) 16 | if v.Kind() == reflect.Ptr { 17 | v = v.Elem() 18 | } 19 | 20 | // we only accept structs 21 | if v.Kind() != reflect.Struct { 22 | log.Fatal("ToMap only accepts structs; got %T", v) 23 | } 24 | 25 | typ := v.Type() 26 | 27 | for i := 0; i < v.NumField(); i++ { 28 | // gets us a StructField 29 | fi := typ.Field(i) 30 | tagv := fi.Tag.Get("redis") 31 | 32 | if tagv == "" || tagv == "-" { 33 | continue 34 | } 35 | 36 | val := v.Field(i).Interface() 37 | 38 | if fi.Type == reflect.TypeOf(time.Now()) { 39 | val = val.(time.Time).Unix() 40 | } 41 | 42 | out[tagv] = val 43 | } 44 | 45 | return out 46 | } 47 | 48 | func Bind(data map[string]string, ptr interface{}) error { 49 | if ptr == nil || len(data) == 0 { 50 | return nil 51 | } 52 | 53 | typ := reflect.TypeOf(ptr).Elem() 54 | val := reflect.ValueOf(ptr).Elem() 55 | 56 | // Map 57 | if typ.Kind() == reflect.Map { 58 | for k, v := range data { 59 | val.SetMapIndex(reflect.ValueOf(k), reflect.ValueOf(v[0])) 60 | } 61 | 62 | return nil 63 | } 64 | 65 | // !struct 66 | if typ.Kind() != reflect.Struct { 67 | return errors.New("binding element must be a struct") 68 | } 69 | 70 | for i := 0; i < typ.NumField(); i++ { 71 | typeField := typ.Field(i) 72 | structField := val.Field(i) 73 | 74 | if !structField.CanSet() { 75 | continue 76 | } 77 | 78 | //structFieldKind := structField.Kind() 79 | inputFieldName := typeField.Tag.Get("redis") 80 | 81 | if inputFieldName == "" || inputFieldName == "-" { 82 | continue 83 | } 84 | 85 | inputValue, exists := data[inputFieldName] 86 | 87 | if !exists { 88 | // Go json.Unmarshal supports case insensitive binding. However the 89 | // url params are bound case sensitive which is inconsistent. To 90 | // fix this we must check all of the map values in a 91 | // case-insensitive search. 92 | for k, v := range data { 93 | if strings.EqualFold(k, inputFieldName) { 94 | inputValue = v 95 | exists = true 96 | break 97 | } 98 | } 99 | } 100 | 101 | if !exists { 102 | continue 103 | } 104 | 105 | if err := setWithProperType(typeField.Type.Kind(), inputValue, structField); err != nil { 106 | return err 107 | } 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { 114 | switch valueKind { 115 | case reflect.Ptr: 116 | return setWithProperType(structField.Elem().Kind(), val, structField.Elem()) 117 | case reflect.Int: 118 | return setIntField(val, 0, structField) 119 | case reflect.Int8: 120 | return setIntField(val, 8, structField) 121 | case reflect.Int16: 122 | return setIntField(val, 16, structField) 123 | case reflect.Int32: 124 | return setIntField(val, 32, structField) 125 | case reflect.Int64: 126 | return setIntField(val, 64, structField) 127 | case reflect.Uint: 128 | return setUintField(val, 0, structField) 129 | case reflect.Uint8: 130 | return setUintField(val, 8, structField) 131 | case reflect.Uint16: 132 | return setUintField(val, 16, structField) 133 | case reflect.Uint32: 134 | return setUintField(val, 32, structField) 135 | case reflect.Uint64: 136 | return setUintField(val, 64, structField) 137 | case reflect.Bool: 138 | return setBoolField(val, structField) 139 | case reflect.Float32: 140 | return setFloatField(val, 32, structField) 141 | case reflect.Float64: 142 | return setFloatField(val, 64, structField) 143 | case reflect.String: 144 | structField.SetString(val) 145 | case reflect.Struct: 146 | switch structField.Type() { 147 | case reflect.TypeOf(time.Now()): 148 | timeInt, _ := strconv.Atoi(val) 149 | timeVal := time.Unix(int64(timeInt), 0) 150 | structField.Set(reflect.ValueOf(timeVal)) 151 | default: 152 | return errors.New("unknown type") 153 | } 154 | default: 155 | return errors.New("unknown type") 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func setIntField(value string, bitSize int, field reflect.Value) error { 162 | if value == "" { 163 | value = "0" 164 | } 165 | 166 | intVal, err := strconv.ParseInt(value, 10, bitSize) 167 | 168 | if err == nil { 169 | field.SetInt(intVal) 170 | } 171 | 172 | return err 173 | } 174 | 175 | func setUintField(value string, bitSize int, field reflect.Value) error { 176 | if value == "" { 177 | value = "0" 178 | } 179 | 180 | uintVal, err := strconv.ParseUint(value, 10, bitSize) 181 | 182 | if err == nil { 183 | field.SetUint(uintVal) 184 | } 185 | 186 | return err 187 | } 188 | 189 | func setBoolField(value string, field reflect.Value) error { 190 | if value == "" { 191 | value = "false" 192 | } 193 | 194 | boolVal, err := strconv.ParseBool(value) 195 | 196 | if err == nil { 197 | field.SetBool(boolVal) 198 | } 199 | 200 | return err 201 | } 202 | 203 | func setFloatField(value string, bitSize int, field reflect.Value) error { 204 | if value == "" { 205 | value = "0.0" 206 | } 207 | 208 | floatVal, err := strconv.ParseFloat(value, bitSize) 209 | 210 | if err == nil { 211 | field.SetFloat(floatVal) 212 | } 213 | 214 | return err 215 | } 216 | -------------------------------------------------------------------------------- /utils/chat.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "unicode" 4 | 5 | func IsASCII(s string) bool { 6 | for i := 0; i < len(s); i++ { 7 | if s[i] > unicode.MaxASCII { 8 | // TODO: Send error packet 9 | return false 10 | } 11 | } 12 | 13 | return true 14 | } 15 | -------------------------------------------------------------------------------- /utils/email.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/aws/aws-sdk-go/aws" 7 | "github.com/aws/aws-sdk-go/aws/session" 8 | "github.com/aws/aws-sdk-go/service/ses" 9 | "github.com/matcornic/hermes/v2" 10 | ) 11 | 12 | const ( 13 | // Sender 14 | Sender = "Blueprint " 15 | 16 | // The subject line for the email. 17 | Subject = "Blueprint Playground Confirmation" 18 | 19 | // The character encoding for the email. 20 | CharSet = "UTF-8" 21 | 22 | ReplyTo = "blueprint@hackmit.org" 23 | ) 24 | 25 | func SendConfirmationEmail(recipient string, code int, name string) { 26 | paddedCode := fmt.Sprintf("%06d", code) 27 | 28 | h := hermes.Hermes{ 29 | Theme: new(hermes.Default), 30 | Product: hermes.Product{ 31 | Name: "Blueprint", 32 | Link: "https://blueprint.hackmit.org", 33 | Logo: "https://blueprint-playground-2021.s3.amazonaws.com/utils/logo.png", 34 | }, 35 | } 36 | 37 | email := hermes.Email{ 38 | Body: hermes.Body{ 39 | Name: name, 40 | Intros: []string{ 41 | "Welcome to the Blueprint playground! We're very excited to have you this weekend.", 42 | }, 43 | Actions: []hermes.Action{ 44 | { 45 | Instructions: "Please copy your invite code:", 46 | InviteCode: paddedCode, 47 | }, 48 | }, 49 | Outros: []string{ 50 | "Any questions? Email blueprint@hackmit.org", 51 | }, 52 | }, 53 | } 54 | 55 | html, _ := h.GenerateHTML(email) 56 | plainText, _ := h.GeneratePlainText(email) 57 | 58 | // 2. send email to person 59 | sess, _ := session.NewSession(&aws.Config{ 60 | Region: aws.String("us-east-1")}, 61 | ) 62 | 63 | // Create an SES session. 64 | svc := ses.New(sess) 65 | 66 | // Assemble the email. 67 | input := &ses.SendEmailInput{ 68 | Destination: &ses.Destination{ 69 | CcAddresses: []*string{}, 70 | ToAddresses: []*string{ 71 | aws.String(recipient), 72 | }, 73 | }, 74 | Message: &ses.Message{ 75 | Body: &ses.Body{ 76 | Html: &ses.Content{ 77 | Charset: aws.String(CharSet), 78 | Data: aws.String(html), 79 | }, 80 | Text: &ses.Content{ 81 | Charset: aws.String(CharSet), 82 | Data: aws.String(plainText), 83 | }, 84 | }, 85 | Subject: &ses.Content{ 86 | Charset: aws.String(CharSet), 87 | Data: aws.String(Subject), 88 | }, 89 | }, 90 | ReplyToAddresses: []*string{aws.String(ReplyTo)}, 91 | Source: aws.String(Sender), 92 | } 93 | 94 | // Attempt to send the email. 95 | _, err := svc.SendEmail(input) 96 | 97 | if err != nil { 98 | fmt.Println(err) 99 | } 100 | } 101 | --------------------------------------------------------------------------------