[
  {
    "path": ".github/workflows/release_build.yml",
    "content": "name: Release Server Binary\n\non:\n  release:\n    types: [created]\n\njobs:\n  releases-matrix:\n    name: Release Go Binary\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        goos: [linux, windows, darwin]\n        goarch: ['386', amd64]\n        exclude:\n          - goarch: '386'\n            goos: darwin\n    steps:\n      - uses: actions/checkout@v3\n      - uses: wangyoucao577/go-release-action@v1.38\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          goos: ${{ matrix.goos }}\n          goarch: ${{ matrix.goarch }}\n          project_path: './server'\n          binary_name: 'valkyrie-server'\n          md5sum: false\n          extra_files: ./server/.env.example README.md\n"
  },
  {
    "path": ".github/workflows/server_ci.yml",
    "content": "name: Test & Lint\n\non:\n  push:\n    paths:\n      - 'server/**'\n  pull_request:\n    paths:\n      - 'server/**'\n\njobs:\n  golangci:\n    name: lint\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/setup-go@v4\n        with:\n          go-version: 1.20\n      - uses: actions/checkout@v3\n      - name: golangci-lint\n        uses: golangci/golangci-lint-action@v3\n        with:\n          version: latest\n          args: --timeout=5m\n          working-directory: ./server\n\n  test:\n    name: Test\n    runs-on: ubuntu-latest\n\n    services:\n      postgres:\n        image: postgres:alpine\n        env:\n          POSTGRES_USER: root\n          POSTGRES_PASSWORD: secret\n          POSTGRES_DB: valkyrie\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n      redis:\n        image: 'redis:alpine'\n        ports:\n          - '6379:6379'\n        volumes:\n          - 'redisdata:/data'\n\n    steps:\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20\n        id: go\n\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@v3\n\n      - name: Run Unit Tests\n        working-directory: ./server\n        run: make test\n\n      - name: Run E2E\n        run: make e2e\n        working-directory: ./server\n        env:\n          DATABASE_URL: postgresql://root:secret@localhost:5432/valkyrie?sslmode=disable\n          HANDLER_TIMEOUT: 5\n          MAX_BODY_BYTES: 4194304\n          REDIS_URL: redis://localhost:6379\n          SECRET: jmaijopspahisodphiasdhiahiopsdhoiasdg8a89sdta08sdtg8aosdou\n          CORS_ORIGIN: origin\n"
  },
  {
    "path": ".github/workflows/server_deploy.yml",
    "content": "# name: Deploy [API]\n\n# on:\n#   workflow_run:\n#     workflows: ['Test & Lint']\n#     branches: [main]\n#     types: [completed]\n\n# jobs:\n#   on-success:\n#     runs-on: ubuntu-latest\n#     if: ${{ github.event.workflow_run.conclusion == 'success' }}\n#     steps:\n#       - uses: actions/checkout@v3\n#       - uses: akhileshns/heroku-deploy@v3.12.12\n#         with:\n#           heroku_api_key: ${{secrets.HEROKU_API_KEY}}\n#           heroku_app_name: ${{secrets.HEROKU_APP_NAME}}\n#           heroku_email: ${{secrets.HEROKU_EMAIL}}\n#           appdir: 'server'\n"
  },
  {
    "path": ".github/workflows/web_ci.yml",
    "content": "name: Test & Lint - Web\n\non:\n  push:\n    paths:\n      - 'web/**'\n  pull_request:\n    paths:\n      - 'web/**'\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - name: Use Node.js 20.x\n        uses: actions/setup-node@v3\n        with:\n          node-version: '20.x'\n          cache: 'yarn'\n          cache-dependency-path: web/yarn.lock\n      - run: cd web && yarn install\n      - run: cd web && yarn compile\n      - run: cd web && yarn lint\n      - run: cd web && yarn test --watchAll=false\n"
  },
  {
    "path": ".github/workflows/website-e2e.yml",
    "content": "name: Website E2E\non:\n  workflow_run:\n    workflows: ['Test & Lint - Web']\n    branches: [main]\n    types: [completed]\n\njobs:\n  cypress-run:\n    runs-on: ubuntu-latest\n    services:\n      postgres:\n        image: postgres:alpine\n        env:\n          POSTGRES_USER: root\n          POSTGRES_PASSWORD: secret\n          POSTGRES_DB: valkyrie\n        ports:\n          - 5432:5432\n        options: >-\n          --health-cmd pg_isready\n          --health-interval 10s\n          --health-timeout 5s\n          --health-retries 5\n      redis:\n        image: 'redis:alpine'\n        ports:\n          - '6379:6379'\n        volumes:\n          - 'redisdata:/data'\n\n    steps:\n      - name: Set up Go\n        uses: actions/setup-go@v4\n        with:\n          go-version: 1.20\n        id: go\n\n      - name: Check out code into the Go module directory\n        uses: actions/checkout@v3\n\n      - name: Run Server\n        run: go run github.com/sentrionic/valkyrie &\n        working-directory: ./server\n        env:\n          DATABASE_URL: postgresql://root:secret@localhost:5432/valkyrie?sslmode=disable\n          HANDLER_TIMEOUT: 5\n          MAX_BODY_BYTES: 4194304\n          REDIS_URL: redis://localhost:6379\n          SECRET: jmaijopspahisodphiasdhiahiopsdhoiasdg8a89sdta08sdtg8aosdou\n          PORT: 4000\n          CORS_ORIGIN: http://localhost:3000\n          GIN_MODE: release\n\n      - name: Cypress run\n        uses: cypress-io/github-action@v4\n        with:\n          install-command: yarn\n          start: yarn start\n          wait-on: http://localhost:3000\n          browser: chrome\n          working-directory: ./web\n          headless: true\n        env:\n          CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n          REACT_APP_API: http://localhost:4000\n          REACT_APP_WS: ws://http://localhost:4000/ws\n"
  },
  {
    "path": ".github/workflows/website_deploy.yml",
    "content": "# name: Website Deploy\n\n# on:\n#   workflow_run:\n#     workflows: ['Website E2E']\n#     branches: [main]\n#     types: [completed]\n\n# jobs:\n#   on-success:\n#     runs-on: ubuntu-latest\n#     if: ${{ github.event.workflow_run.conclusion == 'success' }}\n#     steps:\n#       - name: Checkout code\n#         uses: actions/checkout@v3\n#       - name: Use Node.js 18.x\n#         uses: actions/setup-node@v3\n#         with:\n#           node-version: '18.x'\n#           cache: 'yarn'\n#           cache-dependency-path: web/yarn.lock\n#       - run: yarn install\n#         working-directory: ./web\n#       - run: yarn build --if-present\n#         working-directory: ./web\n#       - name: Deploy to netlify\n#         uses: netlify/actions/cli@master\n#         env:\n#           NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}\n#           NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}\n#         with:\n#           args: deploy --dir=web/build --prod\n#           secrets: '[\"NETLIFY_AUTH_TOKEN\", \"NETLIFY_SITE_ID\"]'\n"
  },
  {
    "path": ".gitignore",
    "content": "\n# Created by https://www.toptal.com/developers/gitignore/api/webstorm\n# Edit at https://www.toptal.com/developers/gitignore?templates=webstorm\n\n### WebStorm ###\n# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider\n# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# Gradle and Maven with auto-import\n# When using Gradle or Maven with auto-import, you should exclude module files,\n# since they will be recreated, and may cause churn.  Uncomment if using\n# auto-import.\n# .idea/artifacts\n# .idea/compiler.xml\n# .idea/jarRepositories.xml\n# .idea/modules.xml\n# .idea/*.iml\n# .idea/modules\n# *.iml\n# *.ipr\n\n# CMake\ncmake-build-*/\n\n# Mongo Explorer plugin\n.idea/**/mongoSettings.xml\n\n# File-based project format\n*.iws\n\n# IntelliJ\nout/\n\n# mpeltonen/sbt-idea plugin\n.idea_modules/\n\n# JIRA plugin\natlassian-ide-plugin.xml\n\n# Cursive Clojure plugin\n.idea/replstate.xml\n\n# Crashlytics plugin (for Android Studio and IntelliJ)\ncom_crashlytics_export_strings.xml\ncrashlytics.properties\ncrashlytics-build.properties\nfabric.properties\n\n# Editor-based Rest Client\n.idea/httpRequests\n\n# Android studio 3.1+ serialized cache file\n.idea/caches/build_file_checksums.ser\n\n### WebStorm Patch ###\n# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721\n\n# *.iml\n# modules.xml\n# .idea/misc.xml\n# *.ipr\n\n# Sonarlint plugin\n# https://plugins.jetbrains.com/plugin/7973-sonarlint\n.idea/**/sonarlint/\n\n# SonarQube Plugin\n# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin\n.idea/**/sonarIssues.xml\n\n# Markdown Navigator plugin\n# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced\n.idea/**/markdown-navigator.xml\n.idea/**/markdown-navigator-enh.xml\n.idea/**/markdown-navigator/\n\n# Cache file creation bug\n# See https://youtrack.jetbrains.com/issue/JBR-2257\n.idea/$CACHE_FILE$\n\n# CodeStream plugin\n# https://plugins.jetbrains.com/plugin/12206-codestream\n.idea/codestream.xml\n\n# End of https://www.toptal.com/developers/gitignore/api/webstorm\n\n# Local Netlify folder\n.netlify"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 sentrionic\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "[![Go Report Card](https://goreportcard.com/badge/github.com/sentrionic/Valkyrie)](https://goreportcard.com/report/github.com/sentrionic/Valkyrie)\n\n# Valkyrie\n\n<p align=\"center\">\n  <img src=\"https://harmony-cdn.s3.eu-central-1.amazonaws.com/logo.png\">\n</p>\n\nA [Discord](https://discord.com) clone using [React](https://reactjs.org/) and [Go](https://golang.org/).\n\n**Notes:**\n\n- The design does not fully match the current design of Discord anymore.\n- For the old [Socket.io](https://socket.io/) stack using [NestJS](https://nestjs.com/) check out the [v1](https://github.com/sentrionic/Valkyrie/tree/v1) branch.\n\n## Video\n\nhttps://user-images.githubusercontent.com/38354571/137365365-a7fe91d6-51d7-4739-8742-f68517223f8f.mp4\n\n## Features\n\n- Message, Channel, Server CRUD\n- Authentication using Express Sessions\n- Channel / Websocket Member Protection\n- Realtime Events\n- File Upload (Avatar, Icon, Messages) to S3\n- Direct Messaging\n- Private Channels\n- Friend System\n- Notification System\n- Basic Moderation for the guild owner (delete messages, kick & ban members)\n- Basic Voice Chat (one voice channel per guild + mute & deafen)\n\n## Stack\n\n### Server\n\n- [Gin](https://gin-gonic.com/) for the HTTP server\n- [Gorilla Websockets](https://github.com/gorilla/websocket) for WS communication\n- [Gorm](https://gorm.io/) as the database ORM\n- PostgreSQL to save all data\n- Redis for storing sessions and reset tokens\n- S3 for storing files and Gmail for sending emails\n\n### Web\n\n- React with [Chakra UI](https://chakra-ui.com/)\n- [React Query](https://react-query.tanstack.com/) & [Zustand](https://github.com/pmndrs/zustand) for state management\n- [Typescript](https://www.typescriptlang.org/)\n\nFor the mobile app using Flutter check out [ValkyrieApp](https://github.com/sentrionic/ValkyrieApp)\n\n---\n\n## Installation\n\n### Server\n\nIf you are familiar with `make`, take a look at the `Makefile` to quickly setup the following steps\nor alternatively copy the commands into your CLI.\n\n1. Install Docker and get the Postgresql and Redis containers (`make postgres` && `make redis`)\n2. Start both containers (`make start`) and create a DB (`make createdb`)\n3. Install the latest version of Go and get all the dependencies (`go mod tidy`)\n4. Rename `.env.example` to `.env` and fill in the values\n\n- `Required`\n\n        PORT=4000\n        DATABASE_URL=postgresql://<username>:<password>@localhost:5432/valkyrie\n        REDIS_URL=redis://localhost:6379\n        CORS_ORIGIN=http://localhost:3000\n        SECRET=SUPERSECRET\n        HANDLER_TIMEOUT=5\n        MAX_BODY_BYTES=4194304 # 4MB in Bytes = 4 * 1024 * 1024\n\n- `Optional: Not needed to run the app, but you won't be able to upload files or send emails.`\n\n        AWS_ACCESS_KEY=ACCESS_KEY\n        AWS_SECRET_ACCESS_KEY=SECRET_ACCESS_KEY\n        AWS_STORAGE_BUCKET_NAME=STORAGE_BUCKET_NAME\n        AWS_S3_REGION=S3_REGION\n        GMAIL_USER=GMAIL_USER\n        GMAIL_PASSWORD=GMAIL_PASSWORD\n\n5. Run `go run github.com/sentrionic/valkyrie` to run the server\n\n**Alternatively**: If you only want to run the backend without installing Go and all dependencies, you can download the pre compiled server from the [Release tab](https://github.com/sentrionic/Valkyrie/releases) instead. You will still need to follow the above steps 1, 2 and 4.\n\n### Web\n\n1. Install Node 20 or the LTS version of Node.\n2. Install [yarn](https://classic.yarnpkg.com/lang/en/)\n3. Run `yarn` to install the dependencies\n4. Run `yarn start` to start the client\n5. Go to `localhost:3000`\n\n## Endpoints\n\nOnce the server is running go to `localhost:<PORT>/swagger/index.html` to see all the HTTP endpoints\nand `localhost:<PORT>` for all the websockets events.\n\n## Tests\n\nAll tests are run on all push and pull requests. Only if they are successful it will run the other Github Actions to automatically deploy the updates.\n\n### Server\n\nAll routes in `handler` have tests written for them.\n\nFunction calls in the `service` directory that do not just delegate work to the repository have tests written for them.\n\nRun `go test -v -cover ./service/... ./handler/...` (`make test`) to run all tests\n\nAdditionally this repository includes E2E tests for all successful requests. To run them you\nhave to have Postgres and Redis running in Docker and then run `go test github.com/sentrionic/valkyrie` (`make e2e`).\n\n### Web\n\nMost `useQuery` hooks have tests written for them.\n\nTo run them use `yarn test`.\n\nAdditionally [Cypress](https://www.cypress.io/) is used for E2E testing.\n\nTo run them you need to have the server and the client running.\nAfter that run `yarn cypress` to open the test window.\n\n**Note**: For unkown reasons websockets connection only randomly work during Cypress runs, which makes testing them impossible.\n\n## Credits\n\n[Ben Awad](https://github.com/benawad): The inital project is based on his Slack tutorial series and I always look at his repositories for inspiration.\n\n[Jacob Goodwin](https://github.com/JacobSNGoodwin/memrizr): This backend is built upon his tutorial series and uses his backend structure.\n\n[Jeroen de Kok](https://dev.to/jeroendk/building-a-simple-chat-application-with-websockets-in-go-and-vue-js-gao): The websockets structure is based on his tutorial.\n\n[ericellb](https://github.com/ericellb/React-Discord-Clone): His repository helped me implement voice chat.\n"
  },
  {
    "path": "server/.gitattributes",
    "content": "*.html -linguist-detectable\n*.css -linguist-detectable\n*.js -linguist-detectable"
  },
  {
    "path": "server/.gitignore",
    "content": "# Created by https://www.toptal.com/developers/gitignore/api/go\n# Edit at https://www.toptal.com/developers/gitignore?templates=go\n\n.env\n\n### Go ###\n# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, built with `go test -c`\n*.test\n\n# Output of the go coverage tool, specifically when used with LiteIDE\n*.out\n\n# Dependency directories (remove the comment below to include it)\n# vendor/\n\n### Go Patch ###\n/vendor/\n/Godeps/\n\n# User-specific stuff\n.idea/**/workspace.xml\n.idea/**/tasks.xml\n.idea/**/usage.statistics.xml\n.idea/**/dictionaries\n.idea/**/shelf\n\n# Generated files\n.idea/**/contentModel.xml\n\n# Sensitive or high-churn files\n.idea/**/dataSources/\n.idea/**/dataSources.ids\n.idea/**/dataSources.local.xml\n.idea/**/sqlDataSources.xml\n.idea/**/dynamic.xml\n.idea/**/uiDesigner.xml\n.idea/**/dbnavigator.xml\n\n# Gradle\n.idea/**/gradle.xml\n.idea/**/libraries\n\n# End of https://www.toptal.com/developers/gitignore/api/go"
  },
  {
    "path": "server/.idea/.gitignore",
    "content": "# Default ignored files\n/shelf/\n/workspace.xml\n# Datasource local storage ignored files\n/dataSources/\n/dataSources.local.xml\n# Editor-based HTTP Client requests\n/httpRequests/\n"
  },
  {
    "path": "server/.idea/ValkyrieGo.iml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<module type=\"WEB_MODULE\" version=\"4\">\n  <component name=\"Go\" enabled=\"true\" />\n  <component name=\"NewModuleRootManager\">\n    <content url=\"file://$MODULE_DIR$\" />\n    <orderEntry type=\"inheritedJdk\" />\n    <orderEntry type=\"sourceFolder\" forTests=\"false\" />\n  </component>\n</module>"
  },
  {
    "path": "server/.idea/dataSources.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"DataSourceManagerImpl\" format=\"xml\" multifile-model=\"true\">\n    <data-source source=\"LOCAL\" name=\"DB\" uuid=\"c44e306a-81ee-4f6d-bb8b-be5992634d17\">\n      <driver-ref>postgresql</driver-ref>\n      <synchronize>true</synchronize>\n      <jdbc-driver>org.postgresql.Driver</jdbc-driver>\n      <jdbc-url>jdbc:postgresql://localhost:5432/valkyrie</jdbc-url>\n      <working-dir>$ProjectFileDir$</working-dir>\n    </data-source>\n  </component>\n</project>"
  },
  {
    "path": "server/.idea/modules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"ProjectModuleManager\">\n    <modules>\n      <module fileurl=\"file://$PROJECT_DIR$/.idea/ValkyrieGo.iml\" filepath=\"$PROJECT_DIR$/.idea/ValkyrieGo.iml\" />\n    </modules>\n  </component>\n</project>"
  },
  {
    "path": "server/.idea/vcs.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project version=\"4\">\n  <component name=\"VcsDirectoryMappings\">\n    <mapping directory=\"$PROJECT_DIR$/..\" vcs=\"Git\" />\n  </component>\n</project>"
  },
  {
    "path": "server/Makefile",
    "content": "postgres:\n\tdocker run --name postgres -p 5432:5432 -e POSTGRES_USER=root -e POSTGRES_PASSWORD=password -d postgres:alpine\n\nredis:\n\tdocker run --name redis -d -p 6379:6379 redis:alpine redis-server --save 60 1\n\ncreatedb:\n\tdocker exec -it postgres createdb --username=root --owner=root valkyrie\n\ndropdb:\n\tdocker exec -it postgres dropdb valkyrie\n\nrecreate:\n\tmake dropdb && make createdb\n\nstart:\n\tdocker start postgres && docker start redis\n\ntest:\n\tgo test -v -cover ./service/... ./handler/...\n\ne2e:\n\tgo test github.com/sentrionic/valkyrie\n\nlint:\n\tgolangci-lint run\n\nmock:\n\tmockery --all\n\nbuild:\n\tgo build github.com/sentrionic/valkyrie\n\nfmt:\n\tgo fmt github.com/sentrionic/...\n\nswag:\n\tswag init\n\nworkflow:\n\tmake fmt && make lint && make test"
  },
  {
    "path": "server/config/config.go",
    "content": "package config\n\nimport (\n\t\"context\"\n\t\"github.com/sethvargo/go-envconfig\"\n)\n\ntype Config struct {\n\tDatabaseUrl    string `env:\"DATABASE_URL,required\"`\n\tRedisUrl       string `env:\"REDIS_URL,required\"`\n\tPort           string `env:\"PORT,default=4000\"`\n\tSessionSecret  string `env:\"SECRET,required\"`\n\tDomain         string `env:\"DOMAIN\"`\n\tCorsOrigin     string `env:\"CORS_ORIGIN,required\"`\n\tAccessKey      string `env:\"AWS_ACCESS_KEY\"`\n\tSecretKey      string `env:\"SECRET_KEY\"`\n\tBucketName     string `env:\"BUCKET_NAME\"`\n\tRegion         string `env:\"REGION\"`\n\tGmailUser      string `env:\"GMAIL_USER\"`\n\tGmailPassword  string `env:\"GMAIL_PASSWORD\"`\n\tHandlerTimeOut int64  `env:\"HANDLER_TIMEOUT,default=5\"`\n\tMaxBodyBytes   int64  `env:\"MAX_BODY_BYTES,default=4194304\"`\n}\n\nfunc LoadConfig(ctx context.Context) (config Config, err error) {\n\terr = envconfig.Process(ctx, &config)\n\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "server/data_sources.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/credentials\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/sentrionic/valkyrie/config\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"gorm.io/driver/postgres\"\n\t\"gorm.io/gorm\"\n\t\"log\"\n)\n\ntype dataSources struct {\n\tDB          *gorm.DB\n\tRedisClient *redis.Client\n\tS3Session   *session.Session\n}\n\n// InitDS establishes connections to fields in dataSources\nfunc initDS(ctx context.Context, cfg config.Config) (*dataSources, error) {\n\tlog.Printf(\"Initializing data sources\\n\")\n\n\tlog.Printf(\"Connecting to Postgresql\\n\")\n\tdb, err := gorm.Open(postgres.Open(cfg.DatabaseUrl))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error opening db: %w\", err)\n\t}\n\n\t// Migrate models and setup join tables\n\tif err = db.AutoMigrate(\n\t\t&model.User{},\n\t\t&model.Guild{},\n\t\t&model.Member{},\n\t\t&model.Channel{},\n\t\t&model.DMMember{},\n\t\t&model.Message{},\n\t\t&model.Attachment{},\n\t\t&model.VCMember{},\n\t); err != nil {\n\t\treturn nil, fmt.Errorf(\"error migrating models: %w\", err)\n\t}\n\n\tif err = db.SetupJoinTable(&model.Guild{}, \"Members\", &model.Member{}); err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating join table: %w\", err)\n\t}\n\n\tif err = db.SetupJoinTable(&model.Guild{}, \"VCMembers\", &model.VCMember{}); err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating join table: %w\", err)\n\t}\n\n\t// Initialize redis connection\n\topt, err := redis.ParseURL(cfg.RedisUrl)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error parsing the redis url: %w\", err)\n\t}\n\n\tlog.Println(\"Connecting to Redis\")\n\trdb := redis.NewClient(opt)\n\n\t// verify redis connection\n\t_, err = rdb.Ping(ctx).Result()\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error connecting to redis: %w\", err)\n\t}\n\n\t// Initialize S3 Session\n\tsess, err := session.NewSession(\n\t\t&aws.Config{\n\t\t\tCredentials: credentials.NewStaticCredentials(\n\t\t\t\tcfg.AccessKey,\n\t\t\t\tcfg.SessionSecret,\n\t\t\t\t\"\",\n\t\t\t),\n\t\t\tRegion: aws.String(cfg.Region),\n\t\t},\n\t)\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"error creating s3 session: %w\", err)\n\t}\n\n\treturn &dataSources{\n\t\tDB:          db,\n\t\tRedisClient: rdb,\n\t\tS3Session:   sess,\n\t}, nil\n}\n\n// close to be used in graceful server shutdown\nfunc (d *dataSources) close() error {\n\tif err := d.RedisClient.Close(); err != nil {\n\t\treturn fmt.Errorf(\"error closing Redis Client: %w\", err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/docs/docs.go",
    "content": "// GENERATED BY THE COMMAND ABOVE; DO NOT EDIT\n// This file was generated by swaggo/swag\n\npackage docs\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/alecthomas/template\"\n\t\"github.com/swaggo/swag\"\n)\n\nvar doc = `{\n    \"schemes\": {{ marshal .Schemes }},\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"description\": \"{{.Description}}\",\n        \"title\": \"{{.Title}}\",\n        \"contact\": {},\n        \"license\": {\n            \"name\": \"Apache 2.0\"\n        },\n        \"version\": \"{{.Version}}\"\n    },\n    \"host\": \"{{.Host}}\",\n    \"basePath\": \"{{.BasePath}}\",\n    \"paths\": {\n        \"/account\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Get Current User\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"consumes\": [\n                    \"multipart/form-data\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Update Current User\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Update Account\",\n                        \"name\": \"account\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/EditUser\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/change-password\": {\n            \"put\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Change Current User's Password\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Change Password\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ChangePasswordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/forgot-password\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Forgot Password Request\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Forgot Password\",\n                        \"name\": \"email\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ForgotPasswordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/login\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"User Login\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Login account\",\n                        \"name\": \"account\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/LoginRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/logout\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"User Logout\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Login account\",\n                        \"name\": \"account\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/LoginRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/me/friends\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Get Current User's Friends\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Friend\"\n                            }\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/me/pending\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Get Current User's Friend Requests\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/FriendRequest\"\n                            }\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/register\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Create an Account\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Create account\",\n                        \"name\": \"account\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/RegisterRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/reset-password\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Reset Password\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Reset Password\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ResetPasswordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/{memberId}/friend\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Send Friend Request\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User ID\",\n                        \"name\": \"memberId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Remove Friend\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User ID\",\n                        \"name\": \"memberId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/{memberId}/friend/accept\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Accept Friend's Request\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User ID\",\n                        \"name\": \"memberId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/{memberId}/friend/cancel\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Cancel Friend's Request\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User ID\",\n                        \"name\": \"memberId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/me/dm\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Get User's DMs\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/DirectMessage\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{channelId}\": {\n            \"put\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Edit Channel\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Edit Channel\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ChannelRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{channelId}/dm\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Get or Create DM\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Member ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/DirectMessage\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{channelId}/members\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Get Members of the given Channel\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{guildId}\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Get Guild Channels\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Channel\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Create Channel\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Channel\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{id}\": {\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Delete Channel\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{id}/dm\": {\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Close DM\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"DM Channel ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Get Current User's Guilds\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/GuildResponse\"\n                            }\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/create\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Create Guild\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Create Guild\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/CreateGuildRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/GuildResponse\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/join\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Join Guild\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Join Guild\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/JoinRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/GuildResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}\": {\n            \"put\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Edit Guild\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Edit Guild\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/EditGuildRequest\"\n                        }\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Leave Guild\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/bans\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Get Guild Ban list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/BanResponse\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Ban Member\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Member ID\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/SuccessResponse\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Unban Member\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Member ID\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/SuccessResponse\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/delete\": {\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Delete Guild\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/invite\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Get Guild Invite\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"description\": \"Is Permanent\",\n                        \"name\": \"isPermanent\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Delete all permanent invite links\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/kick\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Kick Member\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Member ID\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/SuccessResponse\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/member\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Get Member Settings\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberSettings\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Edit Member Settings\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Edit Member\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberSettingsRequest\"\n                        }\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/members\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Get Guild Members\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Member\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/messages/{channelId}\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Messages\"\n                ],\n                \"summary\": \"Get Channel Messages\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Cursor Pagination using the createdAt field\",\n                        \"name\": \"cursor\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Message\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Messages\"\n                ],\n                \"summary\": \"Create Messages\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Create Message\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MessageRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/messages/{messageId}\": {\n            \"put\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Messages\"\n                ],\n                \"summary\": \"Edit Messages\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Message ID\",\n                        \"name\": \"messageId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Edit Message\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MessageRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Messages\"\n                ],\n                \"summary\": \"Delete Messages\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Message ID\",\n                        \"name\": \"messageId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"definitions\": {\n        \"Attachment\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"filename\": {\n                    \"type\": \"string\"\n                },\n                \"filetype\": {\n                    \"type\": \"string\"\n                },\n                \"url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"BanResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ChangePasswordRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"confirmNewPassword\": {\n                    \"description\": \"Must be the same as the newPassword value.\",\n                    \"type\": \"string\"\n                },\n                \"currentPassword\": {\n                    \"type\": \"string\"\n                },\n                \"newPassword\": {\n                    \"description\": \"Min 6, max 150 characters.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"Channel\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"hasNotification\": {\n                    \"type\": \"boolean\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"isPublic\": {\n                    \"type\": \"boolean\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ChannelRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"isPublic\": {\n                    \"description\": \"Default is true\",\n                    \"type\": \"boolean\"\n                },\n                \"members\": {\n                    \"description\": \"Array of memberIds\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"name\": {\n                    \"description\": \"Channel Name. 3 to 30 character\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"CreateGuildRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\n                    \"description\": \"Guild Name. 3 to 30 characters\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"DMUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"isFriend\": {\n                    \"type\": \"boolean\"\n                },\n                \"isOnline\": {\n                    \"type\": \"boolean\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"DirectMessage\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/DMUser\"\n                }\n            }\n        },\n        \"EditGuildRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"icon\": {\n                    \"description\": \"The old guild icon url if no new image is selected. Set to null to reset the guild icon\",\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"description\": \"image/png or image/jpeg\",\n                    \"type\": \"string\",\n                    \"format\": \"binary\"\n                },\n                \"name\": {\n                    \"description\": \"Guild Name. 3 to 30 characters\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"EditUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"description\": \"Must be unique\",\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"description\": \"image/png or image/jpeg\",\n                    \"type\": \"string\",\n                    \"format\": \"binary\"\n                },\n                \"username\": {\n                    \"description\": \"Min 3, max 30 characters.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ErrorResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"error\": {\n                    \"$ref\": \"#/definitions/HttpError\"\n                }\n            }\n        },\n        \"ErrorsResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"errors\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/FieldError\"\n                    }\n                }\n            }\n        },\n        \"FieldError\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"field\": {\n                    \"description\": \"The property containing the error\",\n                    \"type\": \"string\"\n                },\n                \"message\": {\n                    \"description\": \"The specific error message\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ForgotPasswordRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"Friend\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"isOnline\": {\n                    \"type\": \"boolean\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"FriendRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"description\": \"1: Incoming, 0: Outgoing\",\n                    \"type\": \"integer\",\n                    \"enum\": [\n                        0,\n                        1\n                    ]\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"GuildResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"default_channel_id\": {\n                    \"type\": \"string\"\n                },\n                \"hasNotification\": {\n                    \"type\": \"boolean\"\n                },\n                \"icon\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"ownerId\": {\n                    \"type\": \"string\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"HttpError\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"message\": {\n                    \"description\": \"The specific error message\",\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"description\": \"The Http Response as a string\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"JoinRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"link\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"LoginRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"description\": \"Must be unique\",\n                    \"type\": \"string\"\n                },\n                \"password\": {\n                    \"description\": \"Min 6, max 150 characters.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"Member\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"color\": {\n                    \"type\": \"string\"\n                },\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"isFriend\": {\n                    \"type\": \"boolean\"\n                },\n                \"isOnline\": {\n                    \"type\": \"boolean\"\n                },\n                \"nickname\": {\n                    \"type\": \"string\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"MemberRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"memberId\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"MemberSettings\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"color\": {\n                    \"type\": \"string\"\n                },\n                \"nickname\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"MemberSettingsRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"color\": {\n                    \"type\": \"string\"\n                },\n                \"nickname\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"Message\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"attachment\": {\n                    \"$ref\": \"#/definitions/Attachment\"\n                },\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"text\": {\n                    \"type\": \"string\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/Member\"\n                }\n            }\n        },\n        \"MessageRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"file\": {\n                    \"description\": \"image/* or audio/*\",\n                    \"type\": \"string\",\n                    \"format\": \"binary\"\n                },\n                \"text\": {\n                    \"description\": \"Maximum 2000 characters\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"RegisterRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"description\": \"Must be unique\",\n                    \"type\": \"string\"\n                },\n                \"password\": {\n                    \"description\": \"Min 6, max 150 characters.\",\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"description\": \"Min 3, max 30 characters.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ResetPasswordRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"confirmNewPassword\": {\n                    \"description\": \"Must be the same as the password value.\",\n                    \"type\": \"string\"\n                },\n                \"newPassword\": {\n                    \"description\": \"Min 6, max 150 characters.\",\n                    \"type\": \"string\"\n                },\n                \"token\": {\n                    \"description\": \"The token the user got from the email.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"SuccessResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"success\": {\n                    \"description\": \"Only returns true, not a json object\",\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"User\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"email\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"isOnline\": {\n                    \"type\": \"boolean\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        }\n    }\n}`\n\ntype swaggerInfo struct {\n\tVersion     string\n\tHost        string\n\tBasePath    string\n\tSchemes     []string\n\tTitle       string\n\tDescription string\n}\n\n// SwaggerInfo holds exported Swagger Info so clients can modify it\nvar SwaggerInfo = swaggerInfo{\n\tVersion:     \"1.0\",\n\tHost:        \"localhost:<PORT>\",\n\tBasePath:    \"/api\",\n\tSchemes:     []string{},\n\tTitle:       \"Valkyrie API\",\n\tDescription: \"Valkyrie REST API Specs. This service uses sessions for authentication\",\n}\n\ntype s struct{}\n\nfunc (s *s) ReadDoc() string {\n\tsInfo := SwaggerInfo\n\tsInfo.Description = strings.Replace(sInfo.Description, \"\\n\", \"\\\\n\", -1)\n\n\tt, err := template.New(\"swagger_info\").Funcs(template.FuncMap{\n\t\t\"marshal\": func(v any) string {\n\t\t\ta, _ := json.Marshal(v)\n\t\t\treturn string(a)\n\t\t},\n\t}).Parse(doc)\n\tif err != nil {\n\t\treturn doc\n\t}\n\n\tvar tpl bytes.Buffer\n\tif err := t.Execute(&tpl, sInfo); err != nil {\n\t\treturn doc\n\t}\n\n\treturn tpl.String()\n}\n\nfunc init() {\n\tswag.Register(swag.Name, &s{})\n}\n"
  },
  {
    "path": "server/docs/swagger.json",
    "content": "{\n    \"swagger\": \"2.0\",\n    \"info\": {\n        \"description\": \"Valkyrie REST API Specs. This service uses sessions for authentication\",\n        \"title\": \"Valkyrie API\",\n        \"contact\": {},\n        \"license\": {\n            \"name\": \"Apache 2.0\"\n        },\n        \"version\": \"1.0\"\n    },\n    \"host\": \"localhost:\\u003cPORT\\u003e\",\n    \"basePath\": \"/api\",\n    \"paths\": {\n        \"/account\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Get Current User\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"consumes\": [\n                    \"multipart/form-data\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Update Current User\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Update Account\",\n                        \"name\": \"account\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/EditUser\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/change-password\": {\n            \"put\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Change Current User's Password\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Change Password\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ChangePasswordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/forgot-password\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Forgot Password Request\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Forgot Password\",\n                        \"name\": \"email\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ForgotPasswordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/login\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"User Login\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Login account\",\n                        \"name\": \"account\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/LoginRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/logout\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"User Logout\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Login account\",\n                        \"name\": \"account\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/LoginRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/me/friends\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Get Current User's Friends\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Friend\"\n                            }\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/me/pending\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Get Current User's Friend Requests\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/FriendRequest\"\n                            }\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/register\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Create an Account\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Create account\",\n                        \"name\": \"account\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/RegisterRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/reset-password\": {\n            \"post\": {\n                \"consumes\": [\n                    \"application/json\"\n                ],\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Account\"\n                ],\n                \"summary\": \"Reset Password\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Reset Password\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ResetPasswordRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/User\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/{memberId}/friend\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Send Friend Request\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User ID\",\n                        \"name\": \"memberId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Remove Friend\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User ID\",\n                        \"name\": \"memberId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/{memberId}/friend/accept\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Accept Friend's Request\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User ID\",\n                        \"name\": \"memberId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/account/{memberId}/friend/cancel\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Friends\"\n                ],\n                \"summary\": \"Cancel Friend's Request\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"User ID\",\n                        \"name\": \"memberId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/me/dm\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Get User's DMs\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/DirectMessage\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{channelId}\": {\n            \"put\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Edit Channel\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Edit Channel\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ChannelRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{channelId}/dm\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Get or Create DM\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Member ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/DirectMessage\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{channelId}/members\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Get Members of the given Channel\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"type\": \"string\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{guildId}\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Get Guild Channels\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Channel\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Create Channel\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Channel\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{id}\": {\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Delete Channel\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/channels/{id}/dm\": {\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Channels\"\n                ],\n                \"summary\": \"Close DM\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"DM Channel ID\",\n                        \"name\": \"id\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Get Current User's Guilds\",\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/GuildResponse\"\n                            }\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/create\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Create Guild\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Create Guild\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/CreateGuildRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/GuildResponse\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/join\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Join Guild\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Join Guild\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/JoinRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/GuildResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}\": {\n            \"put\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Edit Guild\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Edit Guild\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/EditGuildRequest\"\n                        }\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Leave Guild\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/bans\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Get Guild Ban list\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/BanResponse\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Ban Member\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Member ID\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/SuccessResponse\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Unban Member\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Member ID\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/SuccessResponse\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/delete\": {\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Delete Guild\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/invite\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Get Guild Invite\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"boolean\",\n                        \"description\": \"Is Permanent\",\n                        \"name\": \"isPermanent\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"string\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Delete all permanent invite links\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/kick\": {\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Kick Member\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Member ID\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/SuccessResponse\"\n                            }\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/member\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Get Member Settings\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberSettings\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"put\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Members\"\n                ],\n                \"summary\": \"Edit Member Settings\",\n                \"parameters\": [\n                    {\n                        \"description\": \"Edit Member\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MemberSettingsRequest\"\n                        }\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/guilds/{guildId}/members\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Guilds\"\n                ],\n                \"summary\": \"Get Guild Members\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Guild ID\",\n                        \"name\": \"guildId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Member\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/messages/{channelId}\": {\n            \"get\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Messages\"\n                ],\n                \"summary\": \"Get Channel Messages\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Cursor Pagination using the createdAt field\",\n                        \"name\": \"cursor\",\n                        \"in\": \"query\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"type\": \"array\",\n                            \"items\": {\n                                \"$ref\": \"#/definitions/Message\"\n                            }\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"post\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Messages\"\n                ],\n                \"summary\": \"Create Messages\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Channel ID\",\n                        \"name\": \"channelId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Create Message\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MessageRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"201\": {\n                        \"description\": \"Created\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        },\n        \"/messages/{messageId}\": {\n            \"put\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Messages\"\n                ],\n                \"summary\": \"Edit Messages\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Message ID\",\n                        \"name\": \"messageId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    },\n                    {\n                        \"description\": \"Edit Message\",\n                        \"name\": \"request\",\n                        \"in\": \"body\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/MessageRequest\"\n                        }\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"400\": {\n                        \"description\": \"Bad Request\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorsResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            },\n            \"delete\": {\n                \"produces\": [\n                    \"application/json\"\n                ],\n                \"tags\": [\n                    \"Messages\"\n                ],\n                \"summary\": \"Delete Messages\",\n                \"parameters\": [\n                    {\n                        \"type\": \"string\",\n                        \"description\": \"Message ID\",\n                        \"name\": \"messageId\",\n                        \"in\": \"path\",\n                        \"required\": true\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"description\": \"OK\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/SuccessResponse\"\n                        }\n                    },\n                    \"401\": {\n                        \"description\": \"Unauthorized\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"404\": {\n                        \"description\": \"Not Found\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    },\n                    \"500\": {\n                        \"description\": \"Internal Server Error\",\n                        \"schema\": {\n                            \"$ref\": \"#/definitions/ErrorResponse\"\n                        }\n                    }\n                }\n            }\n        }\n    },\n    \"definitions\": {\n        \"Attachment\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"filename\": {\n                    \"type\": \"string\"\n                },\n                \"filetype\": {\n                    \"type\": \"string\"\n                },\n                \"url\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"BanResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ChangePasswordRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"confirmNewPassword\": {\n                    \"description\": \"Must be the same as the newPassword value.\",\n                    \"type\": \"string\"\n                },\n                \"currentPassword\": {\n                    \"type\": \"string\"\n                },\n                \"newPassword\": {\n                    \"description\": \"Min 6, max 150 characters.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"Channel\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"hasNotification\": {\n                    \"type\": \"boolean\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"isPublic\": {\n                    \"type\": \"boolean\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ChannelRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"isPublic\": {\n                    \"description\": \"Default is true\",\n                    \"type\": \"boolean\"\n                },\n                \"members\": {\n                    \"description\": \"Array of memberIds\",\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"type\": \"string\"\n                    }\n                },\n                \"name\": {\n                    \"description\": \"Channel Name. 3 to 30 character\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"CreateGuildRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"name\": {\n                    \"description\": \"Guild Name. 3 to 30 characters\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"DMUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"isFriend\": {\n                    \"type\": \"boolean\"\n                },\n                \"isOnline\": {\n                    \"type\": \"boolean\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"DirectMessage\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/DMUser\"\n                }\n            }\n        },\n        \"EditGuildRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"icon\": {\n                    \"description\": \"The old guild icon url if no new image is selected. Set to null to reset the guild icon\",\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"description\": \"image/png or image/jpeg\",\n                    \"type\": \"string\",\n                    \"format\": \"binary\"\n                },\n                \"name\": {\n                    \"description\": \"Guild Name. 3 to 30 characters\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"EditUser\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"description\": \"Must be unique\",\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"description\": \"image/png or image/jpeg\",\n                    \"type\": \"string\",\n                    \"format\": \"binary\"\n                },\n                \"username\": {\n                    \"description\": \"Min 3, max 30 characters.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ErrorResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"error\": {\n                    \"$ref\": \"#/definitions/HttpError\"\n                }\n            }\n        },\n        \"ErrorsResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"errors\": {\n                    \"type\": \"array\",\n                    \"items\": {\n                        \"$ref\": \"#/definitions/FieldError\"\n                    }\n                }\n            }\n        },\n        \"FieldError\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"field\": {\n                    \"description\": \"The property containing the error\",\n                    \"type\": \"string\"\n                },\n                \"message\": {\n                    \"description\": \"The specific error message\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ForgotPasswordRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"Friend\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"isOnline\": {\n                    \"type\": \"boolean\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"FriendRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"description\": \"1: Incoming, 0: Outgoing\",\n                    \"type\": \"integer\",\n                    \"enum\": [\n                        0,\n                        1\n                    ]\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"GuildResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"default_channel_id\": {\n                    \"type\": \"string\"\n                },\n                \"hasNotification\": {\n                    \"type\": \"boolean\"\n                },\n                \"icon\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"name\": {\n                    \"type\": \"string\"\n                },\n                \"ownerId\": {\n                    \"type\": \"string\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"HttpError\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"message\": {\n                    \"description\": \"The specific error message\",\n                    \"type\": \"string\"\n                },\n                \"type\": {\n                    \"description\": \"The Http Response as a string\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"JoinRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"link\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"LoginRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"description\": \"Must be unique\",\n                    \"type\": \"string\"\n                },\n                \"password\": {\n                    \"description\": \"Min 6, max 150 characters.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"Member\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"color\": {\n                    \"type\": \"string\"\n                },\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"isFriend\": {\n                    \"type\": \"boolean\"\n                },\n                \"isOnline\": {\n                    \"type\": \"boolean\"\n                },\n                \"nickname\": {\n                    \"type\": \"string\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"MemberRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"memberId\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"MemberSettings\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"color\": {\n                    \"type\": \"string\"\n                },\n                \"nickname\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"MemberSettingsRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"color\": {\n                    \"type\": \"string\"\n                },\n                \"nickname\": {\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"Message\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"attachment\": {\n                    \"$ref\": \"#/definitions/Attachment\"\n                },\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"text\": {\n                    \"type\": \"string\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                },\n                \"user\": {\n                    \"$ref\": \"#/definitions/Member\"\n                }\n            }\n        },\n        \"MessageRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"file\": {\n                    \"description\": \"image/* or audio/*\",\n                    \"type\": \"string\",\n                    \"format\": \"binary\"\n                },\n                \"text\": {\n                    \"description\": \"Maximum 2000 characters\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"RegisterRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"email\": {\n                    \"description\": \"Must be unique\",\n                    \"type\": \"string\"\n                },\n                \"password\": {\n                    \"description\": \"Min 6, max 150 characters.\",\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"description\": \"Min 3, max 30 characters.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"ResetPasswordRequest\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"confirmNewPassword\": {\n                    \"description\": \"Must be the same as the password value.\",\n                    \"type\": \"string\"\n                },\n                \"newPassword\": {\n                    \"description\": \"Min 6, max 150 characters.\",\n                    \"type\": \"string\"\n                },\n                \"token\": {\n                    \"description\": \"The token the user got from the email.\",\n                    \"type\": \"string\"\n                }\n            }\n        },\n        \"SuccessResponse\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"success\": {\n                    \"description\": \"Only returns true, not a json object\",\n                    \"type\": \"boolean\"\n                }\n            }\n        },\n        \"User\": {\n            \"type\": \"object\",\n            \"properties\": {\n                \"createdAt\": {\n                    \"type\": \"string\"\n                },\n                \"email\": {\n                    \"type\": \"string\"\n                },\n                \"id\": {\n                    \"type\": \"string\"\n                },\n                \"image\": {\n                    \"type\": \"string\"\n                },\n                \"isOnline\": {\n                    \"type\": \"boolean\"\n                },\n                \"updatedAt\": {\n                    \"type\": \"string\"\n                },\n                \"username\": {\n                    \"type\": \"string\"\n                }\n            }\n        }\n    }\n}"
  },
  {
    "path": "server/docs/swagger.yaml",
    "content": "basePath: /api\ndefinitions:\n  Attachment:\n    properties:\n      filename:\n        type: string\n      filetype:\n        type: string\n      url:\n        type: string\n    type: object\n  BanResponse:\n    properties:\n      id:\n        type: string\n      image:\n        type: string\n      username:\n        type: string\n    type: object\n  ChangePasswordRequest:\n    properties:\n      confirmNewPassword:\n        description: Must be the same as the newPassword value.\n        type: string\n      currentPassword:\n        type: string\n      newPassword:\n        description: Min 6, max 150 characters.\n        type: string\n    type: object\n  Channel:\n    properties:\n      createdAt:\n        type: string\n      hasNotification:\n        type: boolean\n      id:\n        type: string\n      isPublic:\n        type: boolean\n      name:\n        type: string\n      updatedAt:\n        type: string\n    type: object\n  ChannelRequest:\n    properties:\n      isPublic:\n        description: Default is true\n        type: boolean\n      members:\n        description: Array of memberIds\n        items:\n          type: string\n        type: array\n      name:\n        description: Channel Name. 3 to 30 character\n        type: string\n    type: object\n  CreateGuildRequest:\n    properties:\n      name:\n        description: Guild Name. 3 to 30 characters\n        type: string\n    type: object\n  DMUser:\n    properties:\n      id:\n        type: string\n      image:\n        type: string\n      isFriend:\n        type: boolean\n      isOnline:\n        type: boolean\n      username:\n        type: string\n    type: object\n  DirectMessage:\n    properties:\n      id:\n        type: string\n      user:\n        $ref: '#/definitions/DMUser'\n    type: object\n  EditGuildRequest:\n    properties:\n      icon:\n        description: The old guild icon url if no new image is selected. Set to null\n          to reset the guild icon\n        type: string\n      image:\n        description: image/png or image/jpeg\n        format: binary\n        type: string\n      name:\n        description: Guild Name. 3 to 30 characters\n        type: string\n    type: object\n  EditUser:\n    properties:\n      email:\n        description: Must be unique\n        type: string\n      image:\n        description: image/png or image/jpeg\n        format: binary\n        type: string\n      username:\n        description: Min 3, max 30 characters.\n        type: string\n    type: object\n  ErrorResponse:\n    properties:\n      error:\n        $ref: '#/definitions/HttpError'\n    type: object\n  ErrorsResponse:\n    properties:\n      errors:\n        items:\n          $ref: '#/definitions/FieldError'\n        type: array\n    type: object\n  FieldError:\n    properties:\n      field:\n        description: The property containing the error\n        type: string\n      message:\n        description: The specific error message\n        type: string\n    type: object\n  ForgotPasswordRequest:\n    properties:\n      email:\n        type: string\n    type: object\n  Friend:\n    properties:\n      id:\n        type: string\n      image:\n        type: string\n      isOnline:\n        type: boolean\n      username:\n        type: string\n    type: object\n  FriendRequest:\n    properties:\n      id:\n        type: string\n      image:\n        type: string\n      type:\n        description: '1: Incoming, 0: Outgoing'\n        enum:\n        - 0\n        - 1\n        type: integer\n      username:\n        type: string\n    type: object\n  GuildResponse:\n    properties:\n      createdAt:\n        type: string\n      default_channel_id:\n        type: string\n      hasNotification:\n        type: boolean\n      icon:\n        type: string\n      id:\n        type: string\n      name:\n        type: string\n      ownerId:\n        type: string\n      updatedAt:\n        type: string\n    type: object\n  HttpError:\n    properties:\n      message:\n        description: The specific error message\n        type: string\n      type:\n        description: The Http Response as a string\n        type: string\n    type: object\n  JoinRequest:\n    properties:\n      link:\n        type: string\n    type: object\n  LoginRequest:\n    properties:\n      email:\n        description: Must be unique\n        type: string\n      password:\n        description: Min 6, max 150 characters.\n        type: string\n    type: object\n  Member:\n    properties:\n      color:\n        type: string\n      createdAt:\n        type: string\n      id:\n        type: string\n      image:\n        type: string\n      isFriend:\n        type: boolean\n      isOnline:\n        type: boolean\n      nickname:\n        type: string\n      updatedAt:\n        type: string\n      username:\n        type: string\n    type: object\n  MemberRequest:\n    properties:\n      memberId:\n        type: string\n    type: object\n  MemberSettings:\n    properties:\n      color:\n        type: string\n      nickname:\n        type: string\n    type: object\n  MemberSettingsRequest:\n    properties:\n      color:\n        type: string\n      nickname:\n        type: string\n    type: object\n  Message:\n    properties:\n      attachment:\n        $ref: '#/definitions/Attachment'\n      createdAt:\n        type: string\n      id:\n        type: string\n      text:\n        type: string\n      updatedAt:\n        type: string\n      user:\n        $ref: '#/definitions/Member'\n    type: object\n  MessageRequest:\n    properties:\n      file:\n        description: image/* or audio/*\n        format: binary\n        type: string\n      text:\n        description: Maximum 2000 characters\n        type: string\n    type: object\n  RegisterRequest:\n    properties:\n      email:\n        description: Must be unique\n        type: string\n      password:\n        description: Min 6, max 150 characters.\n        type: string\n      username:\n        description: Min 3, max 30 characters.\n        type: string\n    type: object\n  ResetPasswordRequest:\n    properties:\n      confirmNewPassword:\n        description: Must be the same as the password value.\n        type: string\n      newPassword:\n        description: Min 6, max 150 characters.\n        type: string\n      token:\n        description: The token the user got from the email.\n        type: string\n    type: object\n  SuccessResponse:\n    properties:\n      success:\n        description: Only returns true, not a json object\n        type: boolean\n    type: object\n  User:\n    properties:\n      createdAt:\n        type: string\n      email:\n        type: string\n      id:\n        type: string\n      image:\n        type: string\n      isOnline:\n        type: boolean\n      updatedAt:\n        type: string\n      username:\n        type: string\n    type: object\nhost: localhost:<PORT>\ninfo:\n  contact: {}\n  description: Valkyrie REST API Specs. This service uses sessions for authentication\n  license:\n    name: Apache 2.0\n  title: Valkyrie API\n  version: \"1.0\"\npaths:\n  /account:\n    get:\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/User'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Current User\n      tags:\n      - Account\n    put:\n      consumes:\n      - multipart/form-data\n      parameters:\n      - description: Update Account\n        in: body\n        name: account\n        required: true\n        schema:\n          $ref: '#/definitions/EditUser'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/User'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Update Current User\n      tags:\n      - Account\n  /account/{memberId}/friend:\n    delete:\n      parameters:\n      - description: User ID\n        in: path\n        name: memberId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Remove Friend\n      tags:\n      - Friends\n    post:\n      parameters:\n      - description: User ID\n        in: path\n        name: memberId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Send Friend Request\n      tags:\n      - Friends\n  /account/{memberId}/friend/accept:\n    post:\n      parameters:\n      - description: User ID\n        in: path\n        name: memberId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Accept Friend's Request\n      tags:\n      - Friends\n  /account/{memberId}/friend/cancel:\n    post:\n      parameters:\n      - description: User ID\n        in: path\n        name: memberId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Cancel Friend's Request\n      tags:\n      - Friends\n  /account/change-password:\n    put:\n      consumes:\n      - application/json\n      parameters:\n      - description: Change Password\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/ChangePasswordRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Change Current User's Password\n      tags:\n      - Account\n  /account/forgot-password:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Forgot Password\n        in: body\n        name: email\n        required: true\n        schema:\n          $ref: '#/definitions/ForgotPasswordRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Forgot Password Request\n      tags:\n      - Account\n  /account/login:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Login account\n        in: body\n        name: account\n        required: true\n        schema:\n          $ref: '#/definitions/LoginRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/User'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: User Login\n      tags:\n      - Account\n  /account/logout:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Login account\n        in: body\n        name: account\n        required: true\n        schema:\n          $ref: '#/definitions/LoginRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n      summary: User Logout\n      tags:\n      - Account\n  /account/me/friends:\n    get:\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/Friend'\n            type: array\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Current User's Friends\n      tags:\n      - Friends\n  /account/me/pending:\n    get:\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/FriendRequest'\n            type: array\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Current User's Friend Requests\n      tags:\n      - Friends\n  /account/register:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Create account\n        in: body\n        name: account\n        required: true\n        schema:\n          $ref: '#/definitions/RegisterRequest'\n      produces:\n      - application/json\n      responses:\n        \"201\":\n          description: Created\n          schema:\n            $ref: '#/definitions/User'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Create an Account\n      tags:\n      - Account\n  /account/reset-password:\n    post:\n      consumes:\n      - application/json\n      parameters:\n      - description: Reset Password\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/ResetPasswordRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/User'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Reset Password\n      tags:\n      - Account\n  /channels/{channelId}:\n    put:\n      parameters:\n      - description: Channel ID\n        in: path\n        name: channelId\n        required: true\n        type: string\n      - description: Edit Channel\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/ChannelRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Edit Channel\n      tags:\n      - Channels\n  /channels/{channelId}/dm:\n    post:\n      parameters:\n      - description: Member ID\n        in: path\n        name: channelId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/DirectMessage'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get or Create DM\n      tags:\n      - Channels\n  /channels/{channelId}/members:\n    get:\n      parameters:\n      - description: Channel ID\n        in: path\n        name: channelId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              type: string\n            type: array\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Members of the given Channel\n      tags:\n      - Channels\n  /channels/{guildId}:\n    get:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/Channel'\n            type: array\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Guild Channels\n      tags:\n      - Channels\n    post:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/Channel'\n            type: array\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Create Channel\n      tags:\n      - Channels\n  /channels/{id}:\n    delete:\n      parameters:\n      - description: Channel ID\n        in: path\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Delete Channel\n      tags:\n      - Channels\n  /channels/{id}/dm:\n    delete:\n      parameters:\n      - description: DM Channel ID\n        in: path\n        name: id\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Close DM\n      tags:\n      - Channels\n  /channels/me/dm:\n    get:\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/DirectMessage'\n            type: array\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get User's DMs\n      tags:\n      - Channels\n  /guilds:\n    get:\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/GuildResponse'\n            type: array\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Current User's Guilds\n      tags:\n      - Guilds\n  /guilds/{guildId}:\n    delete:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Leave Guild\n      tags:\n      - Guilds\n    put:\n      parameters:\n      - description: Edit Guild\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/EditGuildRequest'\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Edit Guild\n      tags:\n      - Guilds\n  /guilds/{guildId}/bans:\n    delete:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      - description: Member ID\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/MemberRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/SuccessResponse'\n            type: array\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Unban Member\n      tags:\n      - Members\n    get:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/BanResponse'\n            type: array\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Guild Ban list\n      tags:\n      - Members\n    post:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      - description: Member ID\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/MemberRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/SuccessResponse'\n            type: array\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Ban Member\n      tags:\n      - Members\n  /guilds/{guildId}/delete:\n    delete:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Delete Guild\n      tags:\n      - Guilds\n  /guilds/{guildId}/invite:\n    delete:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Delete all permanent invite links\n      tags:\n      - Guilds\n    get:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      - description: Is Permanent\n        in: query\n        name: isPermanent\n        type: boolean\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            type: string\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Guild Invite\n      tags:\n      - Guilds\n  /guilds/{guildId}/kick:\n    post:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      - description: Member ID\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/MemberRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/SuccessResponse'\n            type: array\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Kick Member\n      tags:\n      - Members\n  /guilds/{guildId}/member:\n    get:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/MemberSettings'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Member Settings\n      tags:\n      - Members\n    put:\n      parameters:\n      - description: Edit Member\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/MemberSettingsRequest'\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Edit Member Settings\n      tags:\n      - Members\n  /guilds/{guildId}/members:\n    get:\n      parameters:\n      - description: Guild ID\n        in: path\n        name: guildId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/Member'\n            type: array\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Guild Members\n      tags:\n      - Guilds\n  /guilds/create:\n    post:\n      parameters:\n      - description: Create Guild\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/CreateGuildRequest'\n      produces:\n      - application/json\n      responses:\n        \"201\":\n          description: Created\n          schema:\n            items:\n              $ref: '#/definitions/GuildResponse'\n            type: array\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Create Guild\n      tags:\n      - Guilds\n  /guilds/join:\n    post:\n      parameters:\n      - description: Join Guild\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/JoinRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/GuildResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Join Guild\n      tags:\n      - Guilds\n  /messages/{channelId}:\n    get:\n      parameters:\n      - description: Channel ID\n        in: path\n        name: channelId\n        required: true\n        type: string\n      - description: Cursor Pagination using the createdAt field\n        in: query\n        name: cursor\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            items:\n              $ref: '#/definitions/Message'\n            type: array\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Get Channel Messages\n      tags:\n      - Messages\n    post:\n      parameters:\n      - description: Channel ID\n        in: path\n        name: channelId\n        required: true\n        type: string\n      - description: Create Message\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/MessageRequest'\n      produces:\n      - application/json\n      responses:\n        \"201\":\n          description: Created\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Create Messages\n      tags:\n      - Messages\n  /messages/{messageId}:\n    delete:\n      parameters:\n      - description: Message ID\n        in: path\n        name: messageId\n        required: true\n        type: string\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Delete Messages\n      tags:\n      - Messages\n    put:\n      parameters:\n      - description: Message ID\n        in: path\n        name: messageId\n        required: true\n        type: string\n      - description: Edit Message\n        in: body\n        name: request\n        required: true\n        schema:\n          $ref: '#/definitions/MessageRequest'\n      produces:\n      - application/json\n      responses:\n        \"200\":\n          description: OK\n          schema:\n            $ref: '#/definitions/SuccessResponse'\n        \"400\":\n          description: Bad Request\n          schema:\n            $ref: '#/definitions/ErrorsResponse'\n        \"401\":\n          description: Unauthorized\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"404\":\n          description: Not Found\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n        \"500\":\n          description: Internal Server Error\n          schema:\n            $ref: '#/definitions/ErrorResponse'\n      summary: Edit Messages\n      tags:\n      - Messages\nswagger: \"2.0\"\n"
  },
  {
    "path": "server/e2e_test.go",
    "content": "package main\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/joho/godotenv\"\n\t\"github.com/sentrionic/valkyrie/config\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc setupTest(t *testing.T) *gin.Engine {\n\tgin.SetMode(gin.ReleaseMode)\n\n\tctx := context.Background()\n\n\t// Load config for local testing, ignore for CI\n\t_ = godotenv.Load()\n\n\tcfg, err := config.LoadConfig(ctx)\n\tassert.NoError(t, err)\n\n\tds, err := initDS(ctx, cfg)\n\tassert.NoError(t, err)\n\n\trouter, err := inject(ds, cfg)\n\tassert.NoError(t, err)\n\n\treturn router\n}\n\nfunc TestMain_AccountE2E(t *testing.T) {\n\trouter := setupTest(t)\n\n\tauthUser := fixture.GetMockUser()\n\tcookie := \"\"\n\n\tmockUsername := fixture.Username()\n\tnewPassword := fixture.RandStr(10)\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetupRequest  func() (*http.Request, error)\n\t\tsetupHeaders  func(t *testing.T, request *http.Request)\n\t\tcheckResponse func(recorder *httptest.ResponseRecorder)\n\t}{\n\t\t{\n\t\t\tname: \"Register Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": authUser.Username,\n\t\t\t\t\t\"password\": authUser.Password,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tassert.Contains(t, recorder.Header(), \"Set-Cookie\")\n\t\t\t\tauthUser.ID = respBody.ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Login Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"password\": authUser.Password,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/login\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tassert.Contains(t, recorder.Header(), \"Set-Cookie\")\n\n\t\t\t\tcookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/account\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Edit Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tform := url.Values{}\n\t\t\t\tform.Add(\"username\", mockUsername)\n\t\t\t\tform.Add(\"email\", authUser.Email)\n\n\t\t\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/account\", strings.NewReader(form.Encode()))\n\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\trequest.Form = form\n\n\t\t\t\treturn request, nil\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockUsername, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Change Password\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"currentPassword\":    authUser.Password,\n\t\t\t\t\t\"newPassword\":        newPassword,\n\t\t\t\t\t\"confirmNewPassword\": newPassword,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPut, \"/api/account/change-password\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Login to verify changes\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"password\": newPassword,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/login\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockUsername, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Logout\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/logout\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check session is invalid now\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/account\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusUnauthorized, recorder.Code)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trr := httptest.NewRecorder()\n\t\t\trequest, err := tc.setupRequest()\n\t\t\ttc.setupHeaders(t, request)\n\t\t\tassert.NoError(t, err)\n\t\t\trouter.ServeHTTP(rr, request)\n\t\t\ttc.checkResponse(rr)\n\t\t})\n\t}\n}\n\nfunc TestMain_FriendsE2E(t *testing.T) {\n\trouter := setupTest(t)\n\n\tauthUser := fixture.GetMockUser()\n\tcookie := \"\"\n\n\tmockUser := fixture.GetMockUser()\n\tmockUserCookie := \"\"\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetupRequest  func() (*http.Request, error)\n\t\tsetupHeaders  func(t *testing.T, request *http.Request)\n\t\tcheckResponse func(recorder *httptest.ResponseRecorder)\n\t}{\n\t\t{\n\t\t\tname: \"Register Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": authUser.Username,\n\t\t\t\t\t\"password\": authUser.Password,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tassert.Contains(t, recorder.Header(), \"Set-Cookie\")\n\t\t\t\tauthUser.ID = respBody.ID\n\t\t\t\tcookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Register Friend\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": mockUser.Username,\n\t\t\t\t\t\"password\": mockUser.Password,\n\t\t\t\t\t\"email\":    mockUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, mockUser.Email, respBody.Email)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.Equal(t, mockUser.Image, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tmockUser.ID = respBody.ID\n\t\t\t\tmockUserCookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Send friend request\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPost, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get friend requests\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/account/me/pending\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.FriendRequest{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\trequests := *respBody\n\t\t\t\tassert.Equal(t, 1, len(requests))\n\t\t\t\trequest := requests[0]\n\n\t\t\t\tassert.Equal(t, authUser.Username, request.Username)\n\t\t\t\tassert.Equal(t, authUser.ID, request.Id)\n\t\t\t\tassert.Equal(t, authUser.Image, request.Image)\n\t\t\t\tassert.Equal(t, model.Incoming, request.Type)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Accept friend request\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/account/%s/friend/accept\", authUser.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPost, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check that requests are empty now\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/account/me/pending\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.FriendRequest{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\trequests := *respBody\n\t\t\t\tassert.Equal(t, 0, len(requests))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get friends\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/account/me/friends\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.Friend{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tfriends := *respBody\n\t\t\t\tassert.Equal(t, 1, len(friends))\n\t\t\t\tfriend := friends[0]\n\n\t\t\t\tassert.Equal(t, authUser.Username, friend.Username)\n\t\t\t\tassert.Equal(t, authUser.ID, friend.Id)\n\t\t\t\tassert.Equal(t, authUser.Image, friend.Image)\n\t\t\t\tassert.True(t, friend.IsOnline)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Remove friend\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/account/%s/friend\", authUser.ID)\n\t\t\t\treturn http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Confirm friends list is empty\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/account/me/friends\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.Friend{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tfriends := *respBody\n\t\t\t\tassert.Equal(t, 0, len(friends))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Send another friend request\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPost, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get authUser's friend requests\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/account/me/pending\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.FriendRequest{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\trequests := *respBody\n\t\t\t\tassert.Equal(t, 1, len(requests))\n\t\t\t\trequest := requests[0]\n\n\t\t\t\tassert.Equal(t, mockUser.Username, request.Username)\n\t\t\t\tassert.Equal(t, mockUser.ID, request.Id)\n\t\t\t\tassert.Equal(t, mockUser.Image, request.Image)\n\t\t\t\tassert.Equal(t, model.Outgoing, request.Type)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Cancel friend request\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/account/%s/friend/accept\", mockUser.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPost, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trr := httptest.NewRecorder()\n\t\t\trequest, err := tc.setupRequest()\n\t\t\ttc.setupHeaders(t, request)\n\t\t\tassert.NoError(t, err)\n\t\t\trouter.ServeHTTP(rr, request)\n\t\t\ttc.checkResponse(rr)\n\t\t})\n\t}\n}\n\nfunc TestMain_GuildsE2E(t *testing.T) {\n\trouter := setupTest(t)\n\n\tauthUser := fixture.GetMockUser()\n\tcookie := \"\"\n\n\tmockUser := fixture.GetMockUser()\n\tmockUserCookie := \"\"\n\n\tmockGuild := fixture.GetMockGuild(\"\")\n\tinviteLink := \"\"\n\n\tmockName := fixture.Username()\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetupRequest  func() (*http.Request, error)\n\t\tsetupHeaders  func(t *testing.T, request *http.Request)\n\t\tcheckResponse func(recorder *httptest.ResponseRecorder)\n\t}{\n\t\t{\n\t\t\tname: \"Register Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": authUser.Username,\n\t\t\t\t\t\"password\": authUser.Password,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tassert.Contains(t, recorder.Header(), \"Set-Cookie\")\n\t\t\t\tauthUser.ID = respBody.ID\n\t\t\t\tcookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Register Member\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": mockUser.Username,\n\t\t\t\t\t\"password\": mockUser.Password,\n\t\t\t\t\t\"email\":    mockUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, mockUser.Email, respBody.Email)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.Equal(t, mockUser.Image, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tmockUser.ID = respBody.ID\n\t\t\t\tmockUserCookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Create Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"name\": mockGuild.Name,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, respBody.Name)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.OwnerId)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Nil(t, respBody.Icon)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\t\t\t\tassert.NotNil(t, respBody.DefaultChannelId)\n\n\t\t\t\tmockGuild.ID = respBody.Id\n\t\t\t\tmockGuild.OwnerId = authUser.ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get authUser's guilds\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tguilds := *respBody\n\t\t\t\tassert.Equal(t, 1, len(guilds))\n\t\t\t\tguild := guilds[0]\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, guild.Name)\n\t\t\t\tassert.Equal(t, mockGuild.ID, guild.Id)\n\t\t\t\tassert.Equal(t, mockGuild.OwnerId, guild.OwnerId)\n\t\t\t\tassert.Nil(t, guild.Icon)\n\t\t\t\tassert.NotNil(t, guild.CreatedAt)\n\t\t\t\tassert.NotNil(t, guild.UpdatedAt)\n\t\t\t\tassert.False(t, guild.HasNotification)\n\t\t\t\tassert.NotNil(t, guild.DefaultChannelId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Edit Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tform := url.Values{}\n\t\t\t\tform.Add(\"name\", mockName)\n\n\t\t\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/guilds/\"+mockGuild.ID, strings.NewReader(form.Encode()))\n\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\trequest.Form = form\n\n\t\t\t\treturn request, nil\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get guild invite\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), &inviteLink)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, inviteLink)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Join Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"link\": inviteLink,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockName, respBody.Name)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.OwnerId)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Nil(t, respBody.Icon)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\t\t\t\tassert.NotNil(t, respBody.DefaultChannelId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check mockUser is in the guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tguilds := *respBody\n\t\t\t\tassert.Equal(t, 1, len(guilds))\n\t\t\t\tguild := guilds[0]\n\n\t\t\t\tassert.Equal(t, mockName, guild.Name)\n\t\t\t\tassert.Equal(t, mockGuild.ID, guild.Id)\n\t\t\t\tassert.Equal(t, mockGuild.OwnerId, guild.OwnerId)\n\t\t\t\tassert.Nil(t, guild.Icon)\n\t\t\t\tassert.NotNil(t, guild.CreatedAt)\n\t\t\t\tassert.NotNil(t, guild.UpdatedAt)\n\t\t\t\tassert.False(t, guild.HasNotification)\n\t\t\t\tassert.NotNil(t, guild.DefaultChannelId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Leave Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodDelete, \"/api/guilds/\"+mockGuild.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check mockUser is in no guilds anymore\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tguilds := *respBody\n\t\t\t\tassert.Equal(t, 0, len(guilds))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get guild members\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/members\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.MemberResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tmembers := *respBody\n\t\t\t\tassert.Equal(t, 1, len(members))\n\t\t\t\tmember := members[0]\n\n\t\t\t\tassert.Equal(t, authUser.Username, member.Username)\n\t\t\t\tassert.Equal(t, authUser.ID, member.Id)\n\t\t\t\tassert.True(t, member.IsOnline)\n\t\t\t\tassert.NotNil(t, member.Image)\n\t\t\t\tassert.NotNil(t, member.CreatedAt)\n\t\t\t\tassert.NotNil(t, member.UpdatedAt)\n\t\t\t\tassert.Nil(t, member.Nickname)\n\t\t\t\tassert.Nil(t, member.Color)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Delete Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/delete\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Verify that there are no more guilds\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tguilds := *respBody\n\t\t\t\tassert.Len(t, guilds, 0)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trr := httptest.NewRecorder()\n\t\t\trequest, err := tc.setupRequest()\n\t\t\ttc.setupHeaders(t, request)\n\t\t\tassert.NoError(t, err)\n\t\t\trouter.ServeHTTP(rr, request)\n\t\t\ttc.checkResponse(rr)\n\t\t})\n\t}\n}\n\nfunc TestMain_MembersE2E(t *testing.T) {\n\trouter := setupTest(t)\n\n\tauthUser := fixture.GetMockUser()\n\tcookie := \"\"\n\n\tmockUser := fixture.GetMockUser()\n\tmockUserCookie := \"\"\n\n\tmockGuild := fixture.GetMockGuild(\"\")\n\tinviteLink := \"\"\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetupRequest  func() (*http.Request, error)\n\t\tsetupHeaders  func(t *testing.T, request *http.Request)\n\t\tcheckResponse func(recorder *httptest.ResponseRecorder)\n\t}{\n\t\t{\n\t\t\tname: \"Register Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": authUser.Username,\n\t\t\t\t\t\"password\": authUser.Password,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tassert.Contains(t, recorder.Header(), \"Set-Cookie\")\n\t\t\t\tauthUser.ID = respBody.ID\n\t\t\t\tcookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Register Member\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": mockUser.Username,\n\t\t\t\t\t\"password\": mockUser.Password,\n\t\t\t\t\t\"email\":    mockUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, mockUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, mockUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tmockUser.ID = respBody.ID\n\t\t\t\tmockUserCookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Create Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"name\": mockGuild.Name,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, respBody.Name)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.OwnerId)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Nil(t, respBody.Icon)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\t\t\t\tassert.NotNil(t, respBody.DefaultChannelId)\n\n\t\t\t\tmockGuild.ID = respBody.Id\n\t\t\t\tmockGuild.OwnerId = authUser.ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Edit authUser's member settings\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"nickname\": authUser.Username,\n\t\t\t\t\t\"color\":    \"#fff\",\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get authUser's member settings\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &model.MemberSettings{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, *respBody.Nickname)\n\t\t\t\tassert.Equal(t, \"#fff\", *respBody.Color)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get guild invite\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite?isPermanent=true\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), &inviteLink)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, inviteLink)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Join Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"link\": inviteLink,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, respBody.Name)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.OwnerId)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Nil(t, respBody.Icon)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\t\t\t\tassert.NotNil(t, respBody.DefaultChannelId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check mockUser is in the guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tguilds := *respBody\n\t\t\t\tassert.Equal(t, 1, len(guilds))\n\t\t\t\tguild := guilds[0]\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, guild.Name)\n\t\t\t\tassert.Equal(t, mockGuild.ID, guild.Id)\n\t\t\t\tassert.Equal(t, mockGuild.OwnerId, guild.OwnerId)\n\t\t\t\tassert.Nil(t, guild.Icon)\n\t\t\t\tassert.NotNil(t, guild.CreatedAt)\n\t\t\t\tassert.NotNil(t, guild.UpdatedAt)\n\t\t\t\tassert.False(t, guild.HasNotification)\n\t\t\t\tassert.NotNil(t, guild.DefaultChannelId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Kick member\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"memberId\": mockUser.ID,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check mockUser is in no guilds anymore\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tguilds := *respBody\n\t\t\t\tassert.Equal(t, 0, len(guilds))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Rejoin Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"link\": inviteLink,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, respBody.Name)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.OwnerId)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Nil(t, respBody.Icon)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\t\t\t\tassert.NotNil(t, respBody.DefaultChannelId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check mockUser is in the guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tguilds := *respBody\n\t\t\t\tassert.Equal(t, 1, len(guilds))\n\t\t\t\tguild := guilds[0]\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, guild.Name)\n\t\t\t\tassert.Equal(t, mockGuild.ID, guild.Id)\n\t\t\t\tassert.Equal(t, mockGuild.OwnerId, guild.OwnerId)\n\t\t\t\tassert.Nil(t, guild.Icon)\n\t\t\t\tassert.NotNil(t, guild.CreatedAt)\n\t\t\t\tassert.NotNil(t, guild.UpdatedAt)\n\t\t\t\tassert.False(t, guild.HasNotification)\n\t\t\t\tassert.NotNil(t, guild.DefaultChannelId)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Ban member\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"memberId\": mockUser.ID,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check mockUser is in no guilds anymore\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", mockUserCookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tguilds := *respBody\n\t\t\t\tassert.Equal(t, 0, len(guilds))\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get guild ban list\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.BanResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tbans := *respBody\n\t\t\t\tassert.Equal(t, 1, len(bans))\n\t\t\t\tban := bans[0]\n\n\t\t\t\tassert.Equal(t, mockUser.Username, ban.Username)\n\t\t\t\tassert.Equal(t, mockUser.ID, ban.Id)\n\t\t\t\tassert.NotNil(t, ban.Image)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Unban member\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"memberId\": mockUser.ID,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Verify ban list is empty\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\t\t\treturn http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.BanResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tbans := *respBody\n\t\t\t\tassert.Equal(t, 0, len(bans))\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trr := httptest.NewRecorder()\n\t\t\trequest, err := tc.setupRequest()\n\t\t\ttc.setupHeaders(t, request)\n\t\t\tassert.NoError(t, err)\n\t\t\trouter.ServeHTTP(rr, request)\n\t\t\ttc.checkResponse(rr)\n\t\t})\n\t}\n}\n\nfunc TestMain_ChannelsE2E(t *testing.T) {\n\trouter := setupTest(t)\n\n\tauthUser := fixture.GetMockUser()\n\tcookie := \"\"\n\n\tmockGuild := fixture.GetMockGuild(\"\")\n\n\tmockChannel := fixture.GetMockChannel(\"\")\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetupRequest  func() (*http.Request, error)\n\t\tsetupHeaders  func(t *testing.T, request *http.Request)\n\t\tcheckResponse func(recorder *httptest.ResponseRecorder)\n\t}{\n\t\t{\n\t\t\tname: \"Register Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": authUser.Username,\n\t\t\t\t\t\"password\": authUser.Password,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tassert.Contains(t, recorder.Header(), \"Set-Cookie\")\n\t\t\t\tauthUser.ID = respBody.ID\n\t\t\t\tcookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Create Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"name\": mockGuild.Name,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, respBody.Name)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.OwnerId)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Nil(t, respBody.Icon)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\t\t\t\tassert.NotNil(t, respBody.DefaultChannelId)\n\n\t\t\t\tmockGuild.ID = respBody.Id\n\t\t\t\tmockGuild.OwnerId = authUser.ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Create Channel\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"name\": mockChannel.Name,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/channels/\"+mockGuild.ID, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.ChannelResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockChannel.Name, respBody.Name)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.True(t, respBody.IsPublic)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\n\t\t\t\tmockChannel.ID = respBody.Id\n\t\t\t\tmockChannel.GuildID = &mockGuild.ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get guild channels\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/channels/\"+mockGuild.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.ChannelResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tchannels := *respBody\n\t\t\t\tassert.Equal(t, 2, len(channels))\n\t\t\t\tchannel := channels[1]\n\n\t\t\t\tassert.Equal(t, mockChannel.Name, channel.Name)\n\t\t\t\tassert.Equal(t, mockChannel.ID, channel.Id)\n\t\t\t\tassert.NotNil(t, channel.CreatedAt)\n\t\t\t\tassert.NotNil(t, channel.UpdatedAt)\n\t\t\t\tassert.True(t, channel.IsPublic)\n\t\t\t\tassert.False(t, channel.HasNotification)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Edit Channel\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"name\":     mockChannel.Name,\n\t\t\t\t\t\"isPublic\": false,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPut, \"/api/channels/\"+mockChannel.ID, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check that the channel is private now\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/channels/\"+mockGuild.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.ChannelResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tchannels := *respBody\n\t\t\t\tassert.Equal(t, 2, len(channels))\n\t\t\t\tchannel := channels[1]\n\n\t\t\t\tassert.Equal(t, mockChannel.Name, channel.Name)\n\t\t\t\tassert.Equal(t, mockChannel.ID, channel.Id)\n\t\t\t\tassert.NotNil(t, channel.CreatedAt)\n\t\t\t\tassert.NotNil(t, channel.UpdatedAt)\n\t\t\t\tassert.False(t, channel.IsPublic)\n\t\t\t\tassert.False(t, channel.HasNotification)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get private channel members\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/channels/%s/members\", mockChannel.ID)\n\t\t\t\treturn http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\tvar respBody []string\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), &respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tassert.Equal(t, 1, len(respBody))\n\t\t\t\tid := respBody[0]\n\n\t\t\t\tassert.Equal(t, authUser.ID, id)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Delete Channel\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodDelete, \"/api/channels/\"+mockChannel.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Check that the channel is got deleted\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/channels/\"+mockGuild.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.ChannelResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tchannels := *respBody\n\t\t\t\tassert.Equal(t, 1, len(channels))\n\t\t\t\tchannel := channels[0]\n\n\t\t\t\tassert.Equal(t, \"general\", channel.Name)\n\t\t\t\tassert.NotNil(t, channel.Id)\n\t\t\t\tassert.NotNil(t, channel.CreatedAt)\n\t\t\t\tassert.NotNil(t, channel.UpdatedAt)\n\t\t\t\tassert.True(t, channel.IsPublic)\n\t\t\t\tassert.False(t, channel.HasNotification)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trr := httptest.NewRecorder()\n\t\t\trequest, err := tc.setupRequest()\n\t\t\ttc.setupHeaders(t, request)\n\t\t\tassert.NoError(t, err)\n\t\t\trouter.ServeHTTP(rr, request)\n\t\t\ttc.checkResponse(rr)\n\t\t})\n\t}\n}\n\nfunc TestMain_DMsE2E(t *testing.T) {\n\trouter := setupTest(t)\n\n\tauthUser := fixture.GetMockUser()\n\tcookie := \"\"\n\n\tmockUser := fixture.GetMockUser()\n\n\tdmId := \"\"\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetupRequest  func() (*http.Request, error)\n\t\tsetupHeaders  func(t *testing.T, request *http.Request)\n\t\tcheckResponse func(recorder *httptest.ResponseRecorder)\n\t}{\n\t\t{\n\t\t\tname: \"Register Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": authUser.Username,\n\t\t\t\t\t\"password\": authUser.Password,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tassert.Contains(t, recorder.Header(), \"Set-Cookie\")\n\t\t\t\tauthUser.ID = respBody.ID\n\t\t\t\tcookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Register Member\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": mockUser.Username,\n\t\t\t\t\t\"password\": mockUser.Password,\n\t\t\t\t\t\"email\":    mockUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, mockUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, mockUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.Image)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tmockUser.ID = respBody.ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Start a DM\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/channels/%s/dm\", mockUser.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPost, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &model.DirectMessage{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.NotNil(t, respBody.User)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Equal(t, mockUser.Username, respBody.User.Username)\n\t\t\t\tassert.Equal(t, mockUser.ID, respBody.User.Id)\n\t\t\t\tassert.Equal(t, mockUser.Image, respBody.User.Image)\n\t\t\t\tassert.True(t, respBody.User.IsOnline)\n\t\t\t\tassert.False(t, respBody.User.IsFriend)\n\n\t\t\t\tdmId = respBody.Id\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get authUser's DMs\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/channels/me/dm\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.DirectMessage{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tdms := *respBody\n\t\t\t\tassert.Len(t, dms, 1)\n\t\t\t\tdm := dms[0]\n\n\t\t\t\tassert.NotNil(t, dm.User)\n\t\t\t\tassert.NotNil(t, dm.Id)\n\t\t\t\tassert.Equal(t, mockUser.Username, dm.User.Username)\n\t\t\t\tassert.Equal(t, mockUser.ID, dm.User.Id)\n\t\t\t\tassert.Equal(t, mockUser.Image, dm.User.Image)\n\t\t\t\tassert.True(t, dm.User.IsOnline)\n\t\t\t\tassert.False(t, dm.User.IsFriend)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Close DM\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/channels/%s/dm\", dmId)\n\t\t\t\treturn http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Verify the user does not have any open DMs\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/channels/me/dm\", nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.DirectMessage{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tdms := *respBody\n\t\t\t\tassert.Len(t, dms, 0)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get the already existing DM\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treqUrl := fmt.Sprintf(\"/api/channels/%s/dm\", mockUser.ID)\n\t\t\t\treturn http.NewRequest(http.MethodPost, reqUrl, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &model.DirectMessage{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.NotNil(t, respBody.User)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Equal(t, mockUser.Username, respBody.User.Username)\n\t\t\t\tassert.Equal(t, mockUser.ID, respBody.User.Id)\n\t\t\t\tassert.Equal(t, mockUser.Image, respBody.User.Image)\n\t\t\t\tassert.True(t, respBody.User.IsOnline)\n\t\t\t\tassert.False(t, respBody.User.IsFriend)\n\t\t\t\tassert.Equal(t, dmId, respBody.Id)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trr := httptest.NewRecorder()\n\t\t\trequest, err := tc.setupRequest()\n\t\t\ttc.setupHeaders(t, request)\n\t\t\tassert.NoError(t, err)\n\t\t\trouter.ServeHTTP(rr, request)\n\t\t\ttc.checkResponse(rr)\n\t\t})\n\t}\n}\n\nfunc TestMain_MessagesE2E(t *testing.T) {\n\trouter := setupTest(t)\n\n\tauthUser := fixture.GetMockUser()\n\tcookie := \"\"\n\n\tmockGuild := fixture.GetMockGuild(\"\")\n\tmockChannel := fixture.GetMockChannel(\"\")\n\n\tmockMessage := fixture.GetMockMessage(\"\", \"\")\n\tmockText := fixture.RandStringRunes(10)\n\n\ttestCases := []struct {\n\t\tname          string\n\t\tsetupRequest  func() (*http.Request, error)\n\t\tsetupHeaders  func(t *testing.T, request *http.Request)\n\t\tcheckResponse func(recorder *httptest.ResponseRecorder)\n\t}{\n\t\t{\n\t\t\tname: \"Register Account\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"username\": authUser.Username,\n\t\t\t\t\t\"password\": authUser.Password,\n\t\t\t\t\t\"email\":    authUser.Email,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.User{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, authUser.Username, respBody.Username)\n\t\t\t\tassert.Equal(t, authUser.Email, respBody.Email)\n\t\t\t\tassert.Equal(t, authUser.Image, respBody.Image)\n\t\t\t\tassert.True(t, respBody.IsOnline)\n\t\t\t\tassert.NotNil(t, respBody.ID)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\n\t\t\t\tassert.Contains(t, recorder.Header(), \"Set-Cookie\")\n\t\t\t\tauthUser.ID = respBody.ID\n\t\t\t\tcookie = recorder.Header().Get(\"Set-Cookie\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Create Guild\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"name\": mockGuild.Name,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.GuildResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockGuild.Name, respBody.Name)\n\t\t\t\tassert.Equal(t, authUser.ID, respBody.OwnerId)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.Nil(t, respBody.Icon)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\t\t\t\tassert.NotNil(t, respBody.DefaultChannelId)\n\n\t\t\t\tmockGuild.ID = respBody.Id\n\t\t\t\tmockGuild.OwnerId = authUser.ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Create Channel\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"name\": mockChannel.Name,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/channels/\"+mockGuild.ID, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody := &model.ChannelResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, mockChannel.Name, respBody.Name)\n\t\t\t\tassert.NotNil(t, respBody.Id)\n\t\t\t\tassert.NotNil(t, respBody.CreatedAt)\n\t\t\t\tassert.NotNil(t, respBody.UpdatedAt)\n\t\t\t\tassert.True(t, respBody.IsPublic)\n\t\t\t\tassert.False(t, respBody.HasNotification)\n\n\t\t\t\tmockChannel.ID = respBody.Id\n\t\t\t\tmockChannel.GuildID = &mockGuild.ID\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Create Message\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"text\": *mockMessage.Text,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPost, \"/api/messages/\"+mockChannel.ID, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusCreated, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Get channel messages\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/messages/\"+mockChannel.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.MessageResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tmessages := *respBody\n\t\t\t\tassert.Len(t, messages, 1)\n\t\t\t\tmessage := messages[0]\n\n\t\t\t\tassert.Equal(t, message.Text, mockMessage.Text)\n\t\t\t\tassert.NotNil(t, message.Id)\n\t\t\t\tassert.NotNil(t, message.CreatedAt)\n\t\t\t\tassert.NotNil(t, message.UpdatedAt)\n\t\t\t\tassert.NotNil(t, message.User)\n\t\t\t\tassert.Nil(t, message.Attachment)\n\n\t\t\t\tauthor := message.User\n\t\t\t\tassert.Equal(t, authUser.Username, author.Username)\n\t\t\t\tassert.Equal(t, authUser.ID, author.Id)\n\t\t\t\tassert.True(t, author.IsOnline)\n\t\t\t\tassert.NotNil(t, author.Image)\n\t\t\t\tassert.NotNil(t, author.CreatedAt)\n\t\t\t\tassert.NotNil(t, author.UpdatedAt)\n\t\t\t\tassert.Nil(t, author.Nickname)\n\t\t\t\tassert.Nil(t, author.Color)\n\n\t\t\t\tmockMessage.ID = message.Id\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Edit Message\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\tdata := gin.H{\n\t\t\t\t\t\"text\": mockText,\n\t\t\t\t}\n\n\t\t\t\treqBody, err := json.Marshal(data)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\treturn http.NewRequest(http.MethodPut, \"/api/messages/\"+mockMessage.ID, bytes.NewBuffer(reqBody))\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Verify message got edited\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/messages/\"+mockChannel.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.MessageResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tmessages := *respBody\n\t\t\t\tassert.Len(t, messages, 1)\n\t\t\t\tmessage := messages[0]\n\n\t\t\t\tassert.Equal(t, *message.Text, mockText)\n\t\t\t\tassert.NotNil(t, message.Id)\n\t\t\t\tassert.NotNil(t, message.CreatedAt)\n\t\t\t\tassert.NotNil(t, message.UpdatedAt)\n\t\t\t\tassert.NotNil(t, message.User)\n\t\t\t\tassert.Nil(t, message.Attachment)\n\n\t\t\t\tauthor := message.User\n\t\t\t\tassert.Equal(t, authUser.Username, author.Username)\n\t\t\t\tassert.Equal(t, authUser.ID, author.Id)\n\t\t\t\tassert.True(t, author.IsOnline)\n\t\t\t\tassert.NotNil(t, author.Image)\n\t\t\t\tassert.NotNil(t, author.CreatedAt)\n\t\t\t\tassert.NotNil(t, author.UpdatedAt)\n\t\t\t\tassert.Nil(t, author.Nickname)\n\t\t\t\tassert.Nil(t, author.Color)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Delete Message\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodDelete, \"/api/messages/\"+mockMessage.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody, err := json.Marshal(true)\n\t\t\t\tassert.NoError(t, err)\n\n\t\t\t\tassert.Equal(t, recorder.Body.Bytes(), respBody)\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Verify message got deleted\",\n\t\t\tsetupRequest: func() (*http.Request, error) {\n\t\t\t\treturn http.NewRequest(http.MethodGet, \"/api/messages/\"+mockChannel.ID, nil)\n\t\t\t},\n\t\t\tsetupHeaders: func(t *testing.T, request *http.Request) {\n\t\t\t\trequest.Header.Add(\"Cookie\", cookie)\n\t\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\t\t},\n\t\t\tcheckResponse: func(recorder *httptest.ResponseRecorder) {\n\t\t\t\tassert.Equal(t, http.StatusOK, recorder.Code)\n\n\t\t\t\trespBody := &[]model.MessageResponse{}\n\t\t\t\terr := json.Unmarshal(recorder.Body.Bytes(), respBody)\n\t\t\t\tassert.NoError(t, err)\n\t\t\t\tassert.NotNil(t, respBody)\n\n\t\t\t\tmessages := *respBody\n\t\t\t\tassert.Len(t, messages, 0)\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\trr := httptest.NewRecorder()\n\t\t\trequest, err := tc.setupRequest()\n\t\t\ttc.setupHeaders(t, request)\n\t\t\tassert.NoError(t, err)\n\t\t\trouter.ServeHTTP(rr, request)\n\t\t\ttc.checkResponse(rr)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/go.mod",
    "content": "module github.com/sentrionic/valkyrie\n\ngo 1.20\n\n// +heroku goVersion go1.20\n\nrequire (\n\tgithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751\n\tgithub.com/aws/aws-sdk-go v1.44.289\n\tgithub.com/bwmarrin/snowflake v0.3.0\n\tgithub.com/disintegration/imaging v1.6.2\n\tgithub.com/gin-contrib/sessions v0.0.5\n\tgithub.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgithub.com/go-openapi/spec v0.20.9 // indirect\n\tgithub.com/go-openapi/swag v0.22.4 // indirect\n\tgithub.com/go-ozzo/ozzo-validation/v4 v4.3.0\n\tgithub.com/gorilla/websocket v1.5.0\n\tgithub.com/joho/godotenv v1.5.1\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/leodido/go-urn v1.2.4 // indirect\n\tgithub.com/lib/pq v1.10.9\n\tgithub.com/mailru/easyjson v0.7.7 // indirect\n\tgithub.com/matoous/go-nanoid v1.5.0\n\tgithub.com/matoous/go-nanoid/v2 v2.0.0\n\tgithub.com/rs/cors v1.9.0 // indirect\n\tgithub.com/stretchr/testify v1.8.3\n\tgithub.com/swaggo/files v1.0.1\n\tgithub.com/swaggo/gin-swagger v1.6.0\n\tgithub.com/swaggo/swag v1.16.1\n\tgithub.com/ulule/limiter/v3 v3.11.2\n\tgolang.org/x/crypto v0.10.0\n\tgolang.org/x/net v0.11.0 // indirect\n\tgolang.org/x/sys v0.9.0 // indirect\n\tgolang.org/x/tools v0.10.0 // indirect\n\tgorm.io/driver/postgres v1.5.2\n\tgorm.io/gorm v1.25.1\n)\n\nrequire (\n\tgithub.com/redis/go-redis/v9 v9.0.5\n\tgithub.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950\n\tgithub.com/sethvargo/go-envconfig v0.9.0\n)\n\nrequire (\n\tgithub.com/KyleBanks/depth v1.2.1 // indirect\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff // indirect\n\tgithub.com/bytedance/sonic v1.9.2 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.2.0 // indirect\n\tgithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.2 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.19.6 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.2 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.14.1 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/gomodule/redigo v2.0.0+incompatible // indirect\n\tgithub.com/gorilla/context v1.1.1 // indirect\n\tgithub.com/gorilla/securecookie v1.1.1 // indirect\n\tgithub.com/gorilla/sessions v1.2.1 // indirect\n\tgithub.com/jackc/pgpassfile v1.0.0 // indirect\n\tgithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect\n\tgithub.com/jackc/pgx/v5 v5.4.1 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/jmespath/go-jmespath v0.4.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.5 // indirect\n\tgithub.com/mattn/go-isatty v0.0.19 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.0.8 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rogpeppe/go-internal v1.10.0 // indirect\n\tgithub.com/stretchr/objx v0.5.0 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.2.11 // indirect\n\tgolang.org/x/arch v0.3.0 // indirect\n\tgolang.org/x/image v0.8.0 // indirect\n\tgolang.org/x/text v0.10.0 // indirect\n\tgoogle.golang.org/protobuf v1.30.0 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "server/go.sum",
    "content": "github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=\ngithub.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/aws/aws-sdk-go v1.44.289 h1:5CVEjiHFvdiVlKPBzv0rjG4zH/21W/onT18R5AH/qx0=\ngithub.com/aws/aws-sdk-go v1.44.289/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=\ngithub.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff h1:RmdPFa+slIr4SCBg4st/l/vZWVe9QJKMXGO60Bxbe04=\ngithub.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=\ngithub.com/bsm/ginkgo/v2 v2.7.0 h1:ItPMPH90RbmZJt5GtkcNvIRuGEdwlBItdNVoyzaNQao=\ngithub.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=\ngithub.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=\ngithub.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=\ngithub.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=\ngithub.com/bytedance/sonic v1.9.2 h1:GDaNjuWSGu09guE9Oql0MSTNhNCLlWwO8y/xM5BzcbM=\ngithub.com/bytedance/sonic v1.9.2/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=\ngithub.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=\ngithub.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=\ngithub.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=\ngithub.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=\ngithub.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=\ngithub.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=\ngithub.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=\ngithub.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2 h1:dyuNlYlG1faymw39NdJddnzJICy6587tiGSVioWhYoE=\ngithub.com/gin-gonic/contrib v0.0.0-20221130124618-7e01895a63f2/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=\ngithub.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=\ngithub.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=\ngithub.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=\ngithub.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=\ngithub.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=\ngithub.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=\ngithub.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=\ngithub.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=\ngithub.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=\ngithub.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=\ngithub.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=\ngithub.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=\ngithub.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=\ngithub.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0 h1:byhDUpfEwjsVQb1vBunvIjh2BHQ9ead57VkAEY4V+Es=\ngithub.com/go-ozzo/ozzo-validation/v4 v4.3.0/go.mod h1:2NKgrcHl3z6cJs+3Oo940FPRiTzuqKbvfrL2RxCj6Ew=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k=\ngithub.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=\ngithub.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=\ngithub.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=\ngithub.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=\ngithub.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=\ngithub.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=\ngithub.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=\ngithub.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=\ngithub.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=\ngithub.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=\ngithub.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=\ngithub.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=\ngithub.com/jackc/pgx/v5 v5.4.1 h1:oKfB/FhuVtit1bBM3zNRRsZ925ZkMN3HXL+LgLUM9lE=\ngithub.com/jackc/pgx/v5 v5.4.1/go.mod h1:q6iHT8uDNXWiFNOlRqJzBTaSH3+2xCXkokxHZC5qWFY=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=\ngithub.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=\ngithub.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=\ngithub.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=\ngithub.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=\ngithub.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=\ngithub.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=\ngithub.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek=\ngithub.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=\ngithub.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=\ngithub.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=\ngithub.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=\ngithub.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=\ngithub.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/redis/go-redis/v9 v9.0.5 h1:CuQcn5HIEeK7BgElubPP8CGtE0KakrnbBSTLjathl5o=\ngithub.com/redis/go-redis/v9 v9.0.5/go.mod h1:WqMKv5vnQbRuZstUwxQI195wHy+t4PuXDOjzMvcuQHk=\ngithub.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=\ngithub.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=\ngithub.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE=\ngithub.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=\ngithub.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950 h1:AqLt1PEuscqbMJkmkfOw1xLlDH0VIQzrDEuOGggv0a4=\ngithub.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950/go.mod h1:gmu40DuK3SLdKUzGOUofS3UDZwyeOUy6ZjPPuaALatw=\ngithub.com/sethvargo/go-envconfig v0.9.0 h1:Q6FQ6hVEeTECULvkJZakq3dZMeBQ3JUpcKMfPQbKMDE=\ngithub.com/sethvargo/go-envconfig v0.9.0/go.mod h1:Iz1Gy1Sf3T64TQlJSvee81qDhf7YIlt8GMUX6yyNFs0=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=\ngithub.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=\ngithub.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=\ngithub.com/swaggo/gin-swagger v1.6.0 h1:y8sxvQ3E20/RCyrXeFfg60r6H0Z+SwpTjMYsMm+zy8M=\ngithub.com/swaggo/gin-swagger v1.6.0/go.mod h1:BG00cCEy294xtVpyIAHG6+e2Qzj/xKlRdOqDkvq0uzo=\ngithub.com/swaggo/swag v1.16.1 h1:fTNRhKstPKxcnoKsytm4sahr8FaYzUcT7i1/3nd/fBg=\ngithub.com/swaggo/swag v1.16.1/go.mod h1:9/LMvHycG3NFHfR6LwvikHv5iFvmPADQ359cKikGxto=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=\ngithub.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA=\ngithub.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=\ngolang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.10.0 h1:LKqV2xt9+kDzSTfOhx4FrkEBcMrAgHSYgzywV9zcGmM=\ngolang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=\ngolang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/image v0.8.0 h1:agUcRXV/+w6L9ryntYYsF2x9fQTMd4T8fiiYXAVW6Jg=\ngolang.org/x/image v0.8.0/go.mod h1:PwLxp3opCYg4WR2WO9P0L6ESnsD6bLTWcw8zanLMVFM=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=\ngolang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=\ngolang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=\ngolang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=\ngolang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=\ngoogle.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0=\ngorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8=\ngorm.io/gorm v1.25.1 h1:nsSALe5Pr+cM3V1qwwQ7rOkw+6UeLrX5O4v3llhHa64=\ngorm.io/gorm v1.25.1/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\n"
  },
  {
    "path": "server/handler/account_handler.go",
    "content": "package handler\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation/v4\"\n\t\"github.com/go-ozzo/ozzo-validation/v4/is\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n/*\n * AccountHandler contains all routes related to account actions (/api/account)\n * that the authenticated user can do\n */\n\n// GetCurrent handler calls services for getting\n// a user's details\n// GetCurrent godoc\n// @Tags Account\n// @Summary Get Current User\n// @Produce  json\n// @Success 200 {object} model.User\n// @Failure 404 {object} model.ErrorResponse\n// @Router /account [get]\nfunc (h *Handler) GetCurrent(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tuser, err := h.userService.Get(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, user)\n}\n\ntype editReq struct {\n\t// Min 3, max 30 characters.\n\tUsername string `form:\"username\"`\n\t// Must be unique\n\tEmail string `form:\"email\"`\n\t// image/png or image/jpeg\n\tImage *multipart.FileHeader `form:\"image\" swaggertype:\"string\" format:\"binary\"`\n} //@name EditUser\n\nfunc (r editReq) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Email, validation.Required, is.EmailFormat),\n\t\tvalidation.Field(&r.Username, validation.Required, validation.Length(3, 30)),\n\t)\n}\n\nfunc (r *editReq) sanitize() {\n\tr.Username = strings.TrimSpace(r.Username)\n\tr.Email = strings.TrimSpace(r.Email)\n\tr.Email = strings.ToLower(r.Email)\n}\n\n// Edit handler edits the users account details\n// Edit godoc\n// @Tags Account\n// @Summary Update Current User\n// @Accept mpfd\n// @Produce  json\n// @Param account body editReq true \"Update Account\"\n// @Success 200 {object} model.User\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /account [put]\nfunc (h *Handler) Edit(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tc.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, h.MaxBodyBytes)\n\n\tvar req editReq\n\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tauthUser, err := h.userService.Get(userId)\n\n\tif err != nil {\n\t\te := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tauthUser.Username = req.Username\n\n\t// New email, check if it's unique\n\tif authUser.Email != req.Email {\n\t\tinUse := h.userService.IsEmailAlreadyInUse(req.Email)\n\n\t\tif inUse {\n\t\t\ttoFieldErrorResponse(c, \"Email\", apperrors.DuplicateEmail)\n\t\t\treturn\n\t\t}\n\t\tauthUser.Email = req.Email\n\t}\n\n\tif req.Image != nil {\n\n\t\t// Validate image mime-type is allowable\n\t\tmimeType := req.Image.Header.Get(\"Content-Type\")\n\n\t\tif valid := isAllowedImageType(mimeType); !valid {\n\t\t\ttoFieldErrorResponse(c, \"Image\", apperrors.InvalidImageType)\n\t\t\treturn\n\t\t}\n\n\t\tdirectory := fmt.Sprintf(\"valkyrie/users/%s\", authUser.ID)\n\t\turl, err := h.userService.ChangeAvatar(req.Image, directory)\n\n\t\tif err != nil {\n\t\t\te := apperrors.NewInternal()\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\t_ = h.userService.DeleteImage(authUser.Image)\n\n\t\tauthUser.Image = url\n\t}\n\n\terr = h.userService.UpdateAccount(authUser)\n\n\tif err != nil {\n\t\te := apperrors.NewInternal()\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, authUser)\n}\n\ntype changeRequest struct {\n\tCurrentPassword string `json:\"currentPassword\"`\n\t// Min 6, max 150 characters.\n\tNewPassword string `json:\"newPassword\"`\n\t// Must be the same as the newPassword value.\n\tConfirmNewPassword string `json:\"confirmNewPassword\"`\n} //@name ChangePasswordRequest\n\nfunc (r changeRequest) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.CurrentPassword, validation.Required, validation.Length(6, 150)),\n\t\tvalidation.Field(&r.NewPassword, validation.Required, validation.Length(6, 150)),\n\t\tvalidation.Field(&r.ConfirmNewPassword, validation.Required, validation.Length(6, 150)),\n\t)\n}\n\nfunc (r *changeRequest) sanitize() {\n\tr.CurrentPassword = strings.TrimSpace(r.CurrentPassword)\n\tr.NewPassword = strings.TrimSpace(r.NewPassword)\n\tr.ConfirmNewPassword = strings.TrimSpace(r.ConfirmNewPassword)\n}\n\n// ChangePassword handler changes the user's password\n// ChangePassword godoc\n// @Tags Account\n// @Summary Change Current User's Password\n// @Accept json\n// @Produce  json\n// @Param request body changeRequest true \"Change Password\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /account/change-password [put]\nfunc (h *Handler) ChangePassword(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tvar req changeRequest\n\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\t// Check if passwords are equal\n\tif req.NewPassword != req.ConfirmNewPassword {\n\t\ttoFieldErrorResponse(c, \"password\", apperrors.PasswordsDoNotMatch)\n\t\treturn\n\t}\n\n\tauthUser, err := h.userService.Get(userId)\n\n\tif err != nil {\n\t\te := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\terr = h.userService.ChangePassword(req.CurrentPassword, req.NewPassword, authUser)\n\n\tif err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n"
  },
  {
    "path": "server/handler/account_handler_test.go",
    "content": "package handler\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/sentrionic/valkyrie/service\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestHandler_GetCurrent(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tuid := service.GenerateId()\n\n\t\tmockUserResp := fixture.GetMockUser()\n\t\tmockUserResp.ID = uid\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUserResp, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(mockUserResp)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"NotFound\", func(t *testing.T) {\n\t\tuid := service.GenerateId()\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(nil, fmt.Errorf(\"some error down call chain\"))\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespErr := apperrors.NewNotFound(\"user\", uid)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": respErr,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, respErr.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertExpectations(t) // assert that UserService.Get was called\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tuid := service.GenerateId()\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(nil, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"Get\", uid)\n\t})\n}\n\nfunc TestHandler_EditAccount(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tuid := service.GenerateId()\n\tmockUser := fixture.GetMockUser()\n\tmockUser.ID = uid\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\trouter := getTestRouter()\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\tnewName := fixture.Username()\n\t\tnewEmail := fixture.Email()\n\n\t\tform := url.Values{}\n\t\tform.Add(\"username\", newName)\n\t\tform.Add(\"email\", newEmail)\n\n\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/account\", strings.NewReader(form.Encode()))\n\t\trequest.Form = form\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"UpdateAccount\")\n\t})\n\n\tt.Run(\"UpdateAccount success\", func(t *testing.T) {\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\tnewName := fixture.Username()\n\t\tnewEmail := fixture.Email()\n\n\t\tbody := &bytes.Buffer{}\n\t\twriter := multipart.NewWriter(body)\n\t\t_ = writer.WriteField(\"username\", newName)\n\t\t_ = writer.WriteField(\"email\", newEmail)\n\n\t\t_ = writer.Close()\n\n\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/account\", body)\n\t\trequest.Header.Set(\"Content-Type\", writer.FormDataContentType())\n\n\t\tmockUser.Username = newName\n\t\tmockUser.Email = newEmail\n\n\t\tUpdateAccountArgs := mock.Arguments{\n\t\t\tmockUser,\n\t\t}\n\n\t\tdbImageURL := \"https://website.com/696292a38f493a4283d1a308e4a11732/84d81/Profile.jpg\"\n\n\t\tmockUserService.\n\t\t\tOn(\"UpdateAccount\", UpdateAccountArgs...).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tuserArg := args.Get(0).(*model.User)\n\t\t\t\tuserArg.Image = dbImageURL\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockUser.Image = dbImageURL\n\t\trespBody, _ := json.Marshal(mockUser)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertCalled(t, \"UpdateAccount\", UpdateAccountArgs...)\n\t})\n\n\tt.Run(\"UpdateAccount Failure\", func(t *testing.T) {\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\tform := url.Values{}\n\t\tform.Add(\"username\", mockUser.Username)\n\t\tform.Add(\"email\", mockUser.Email)\n\n\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/account\", strings.NewReader(form.Encode()))\n\t\trequest.Form = form\n\n\t\tmockError := apperrors.NewInternal()\n\n\t\tmockUserService.\n\t\t\tOn(\"UpdateAccount\", mockUser).\n\t\t\tReturn(mockError)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertCalled(t, \"UpdateAccount\", mockUser)\n\t})\n\n\tt.Run(\"Disallowed mimetype\", func(t *testing.T) {\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.txt\", \"mage/svg+xml\")\n\t\tdefer multipartImageFixture.Close()\n\n\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/account\", multipartImageFixture.MultipartBody)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\t\tmockUserService.AssertNotCalled(t, \"ChangeAvatar\")\n\t})\n\n\tt.Run(\"Email already in use\", func(t *testing.T) {\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\tform := url.Values{}\n\n\t\tduplicateEmail := \"duplicate@example.com\"\n\t\tform.Add(\"username\", mockUser.Username)\n\t\tform.Add(\"email\", duplicateEmail)\n\n\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/account\", strings.NewReader(form.Encode()))\n\t\trequest.Form = form\n\n\t\tmockUserService.\n\t\t\tOn(\"IsEmailAlreadyInUse\", duplicateEmail).\n\t\t\tReturn(true)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(getTestFieldErrorResponse(\"Email\", apperrors.DuplicateEmail))\n\n\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertNotCalled(t, \"UpdateAccount\", mockUser)\n\t})\n}\n\nfunc TestHandler_ChangePassword(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tuid := service.GenerateId()\n\tmockUser := fixture.GetMockUser()\n\tmockUser.ID = uid\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\trouter := getTestRouter()\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"currentPassword\":    \"password\",\n\t\t\t\"newPassword\":        \"password!\",\n\t\t\t\"confirmNewPassword\": \"password!\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/account/change-password\", bytes.NewBuffer(reqBody))\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"ChangePassword\")\n\t})\n\n\tt.Run(\"ChangePassword success\", func(t *testing.T) {\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\t\tcurrentPassword := mockUser.Password\n\t\tnewPassword := \"password!\"\n\n\t\tChangePasswordArgs := mock.Arguments{\n\t\t\tcurrentPassword,\n\t\t\tnewPassword,\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockUserService.\n\t\t\tOn(\"ChangePassword\", ChangePasswordArgs...).\n\t\t\tReturn(nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"currentPassword\":    currentPassword,\n\t\t\t\"newPassword\":        newPassword,\n\t\t\t\"confirmNewPassword\": newPassword,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/account/change-password\", bytes.NewBuffer(reqBody))\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(true)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertCalled(t, \"ChangePassword\", ChangePasswordArgs...)\n\t})\n\n\tt.Run(\"ChangePassword Failure\", func(t *testing.T) {\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\t\tcurrentPassword := mockUser.Password\n\t\tnewPassword := \"password!\"\n\n\t\tChangePasswordArgs := mock.Arguments{\n\t\t\tcurrentPassword,\n\t\t\tnewPassword,\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockUserService.\n\t\t\tOn(\"ChangePassword\", ChangePasswordArgs...).\n\t\t\tReturn(mockError)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"currentPassword\":    currentPassword,\n\t\t\t\"newPassword\":        newPassword,\n\t\t\t\"confirmNewPassword\": newPassword,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/account/change-password\", bytes.NewBuffer(reqBody))\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertCalled(t, \"ChangePassword\", ChangePasswordArgs...)\n\t})\n}\n\nfunc TestHandler_ChangePassword_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tuid := service.GenerateId()\n\tmockUser := fixture.GetMockUser()\n\tmockUser.ID = uid\n\n\trouter := getAuthenticatedTestRouter(uid)\n\n\tmockUserService := new(mocks.UserService)\n\tmockUserService.On(\"Get\", uid).Return(mockUser, nil)\n\n\tNewHandler(&Config{\n\t\tR:            router,\n\t\tUserService:  mockUserService,\n\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t})\n\n\tpassword := fixture.RandStringRunes(6)\n\tconfirmPassword := password\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody gin.H\n\t}{\n\t\t{\n\t\t\tname: \"CurrentPassword required\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"newPassword\":        password,\n\t\t\t\t\"confirmNewPassword\": confirmPassword,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"NewPassword too short\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"currentPassword\":    fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(5),\n\t\t\t\t\"confirmNewPassword\": confirmPassword,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"NewPassword too long\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"currentPassword\":    fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(151),\n\t\t\t\t\"confirmNewPassword\": confirmPassword,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"NewPassword required\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"currentPassword\":    fixture.RandStringRunes(6),\n\t\t\t\t\"confirmNewPassword\": fixture.RandStringRunes(6),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ConfirmNewPassword too short\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"currentPassword\":    fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(6),\n\t\t\t\t\"confirmNewPassword\": fixture.RandStringRunes(5),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ConfirmNewPassword too long\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"currentPassword\":    fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(6),\n\t\t\t\t\"confirmNewPassword\": fixture.RandStringRunes(151),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ConfirmNewPassword required\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"currentPassword\": fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":     fixture.RandStringRunes(6),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"NewPassword and ConfirmNewPassword not equal\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"currentPassword\":    fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(6),\n\t\t\t\t\"confirmNewPassword\": fixture.RandStringRunes(6),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\treqBody, err := json.Marshal(tc.body)\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/account/change-password\", bytes.NewBuffer(reqBody))\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockUserService.AssertNotCalled(t, \"ChangePassword\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/handler/auth_handler.go",
    "content": "package handler\n\nimport (\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-gonic/gin\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation/v4\"\n\t\"github.com/go-ozzo/ozzo-validation/v4/is\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n/*\n * AuthHandler contains all routes related to account actions (/api/account)\n */\n\ntype registerReq struct {\n\t// Must be unique\n\tEmail string `json:\"email\"`\n\t// Min 3, max 30 characters.\n\tUsername string `json:\"username\"`\n\t// Min 6, max 150 characters.\n\tPassword string `json:\"password\"`\n} //@name RegisterRequest\n\nfunc (r registerReq) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Email, validation.Required, is.EmailFormat),\n\t\tvalidation.Field(&r.Username, validation.Required, validation.Length(3, 30)),\n\t\tvalidation.Field(&r.Password, validation.Required, validation.Length(6, 150)),\n\t)\n}\n\nfunc (r *registerReq) sanitize() {\n\tr.Username = strings.TrimSpace(r.Username)\n\tr.Email = strings.TrimSpace(r.Email)\n\tr.Email = strings.ToLower(r.Email)\n\tr.Password = strings.TrimSpace(r.Password)\n}\n\n// Register handler creates a new user\n// Register godoc\n// @Tags Account\n// @Summary Create an Account\n// @Accept  json\n// @Produce  json\n// @Param account body registerReq true \"Create account\"\n// @Success 201 {object} model.User\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /account/register [post]\nfunc (h *Handler) Register(c *gin.Context) {\n\tvar req registerReq\n\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tinitial := &model.User{\n\t\tEmail:    req.Email,\n\t\tUsername: req.Username,\n\t\tPassword: req.Password,\n\t}\n\n\tuser, err := h.userService.Register(initial)\n\n\tif err != nil {\n\t\tif err.Error() == apperrors.NewBadRequest(apperrors.DuplicateEmail).Error() {\n\t\t\ttoFieldErrorResponse(c, \"Email\", apperrors.DuplicateEmail)\n\t\t\treturn\n\t\t}\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tsetUserSession(c, user.ID)\n\n\tc.JSON(http.StatusCreated, user)\n}\n\ntype loginReq struct {\n\t// Must be unique\n\tEmail string `json:\"email\"`\n\t// Min 6, max 150 characters.\n\tPassword string `json:\"password\"`\n} //@name LoginRequest\n\nfunc (r loginReq) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Email, validation.Required, is.EmailFormat),\n\t\tvalidation.Field(&r.Password, validation.Required, validation.Length(6, 150)),\n\t)\n}\n\nfunc (r *loginReq) sanitize() {\n\tr.Email = strings.TrimSpace(r.Email)\n\tr.Email = strings.ToLower(r.Email)\n\tr.Password = strings.TrimSpace(r.Password)\n}\n\n// Login used to authenticate existent user\n// Login godoc\n// @Tags Account\n// @Summary User Login\n// @Accept  json\n// @Produce  json\n// @Param account body loginReq true \"Login account\"\n// @Success 200 {object} model.User\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /account/login [post]\nfunc (h *Handler) Login(c *gin.Context) {\n\tvar req loginReq\n\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tuser, err := h.userService.Login(req.Email, req.Password)\n\n\tif err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tsetUserSession(c, user.ID)\n\n\tc.JSON(http.StatusOK, user)\n}\n\n// Logout handler removes the current session\n// Logout godoc\n// @Tags Account\n// @Summary User Logout\n// @Accept  json\n// @Produce  json\n// @Param account body loginReq true \"Login account\"\n// @Success 200 {object} model.Success\n// @Router /account/logout [post]\nfunc (h *Handler) Logout(c *gin.Context) {\n\tc.Set(\"user\", nil)\n\n\tsession := sessions.Default(c)\n\tsession.Set(\"userId\", \"\")\n\tsession.Clear()\n\tsession.Options(sessions.Options{Path: \"/\", MaxAge: -1})\n\terr := session.Save()\n\n\tif err != nil {\n\t\tlog.Printf(\"error clearing session: %v\\n\", err.Error())\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\ntype forgotRequest struct {\n\tEmail string `json:\"email\"`\n} //@name ForgotPasswordRequest\n\nfunc (r forgotRequest) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Email, validation.Required, is.EmailFormat),\n\t)\n}\n\nfunc (r *forgotRequest) sanitize() {\n\tr.Email = strings.TrimSpace(r.Email)\n\tr.Email = strings.ToLower(r.Email)\n}\n\n// ForgotPassword sends a password reset email to the requested email\n// ForgotPassword godoc\n// @Tags Account\n// @Summary Forgot Password Request\n// @Accept  json\n// @Produce  json\n// @Param email body forgotRequest true \"Forgot Password\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /account/forgot-password [post]\nfunc (h *Handler) ForgotPassword(c *gin.Context) {\n\tvar req forgotRequest\n\tif valid := bindData(c, &req); !valid {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tuser, err := h.userService.GetByEmail(req.Email)\n\n\tif err != nil {\n\t\t// No user with the email found\n\t\tif err.Error() == apperrors.NewNotFound(\"email\", req.Email).Error() {\n\t\t\tc.JSON(http.StatusOK, true)\n\t\t\treturn\n\t\t}\n\n\t\te := apperrors.NewInternal()\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tctx := c.Request.Context()\n\terr = h.userService.ForgotPassword(ctx, user)\n\n\tif err != nil {\n\t\te := apperrors.NewInternal()\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\ntype resetRequest struct {\n\t// The token the user got from the email.\n\tToken string `json:\"token\"`\n\t// Min 6, max 150 characters.\n\tPassword string `json:\"newPassword\"`\n\t// Must be the same as the password value.\n\tConfirmPassword string `json:\"confirmNewPassword\"`\n} //@name ResetPasswordRequest\n\nfunc (r resetRequest) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Token, validation.Required),\n\t\tvalidation.Field(&r.Password, validation.Required, validation.Length(6, 150)),\n\t\tvalidation.Field(&r.ConfirmPassword, validation.Required, validation.Length(6, 150)),\n\t)\n}\n\nfunc (r *resetRequest) sanitize() {\n\tr.Token = strings.TrimSpace(r.Token)\n\tr.Password = strings.TrimSpace(r.Password)\n\tr.ConfirmPassword = strings.TrimSpace(r.ConfirmPassword)\n}\n\n// ResetPassword resets the user's password with the provided token\n// ResetPassword godoc\n// @Tags Account\n// @Summary Reset Password\n// @Accept  json\n// @Produce  json\n// @Param request body resetRequest true \"Reset Password\"\n// @Success 200 {object} model.User\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /account/reset-password [post]\nfunc (h *Handler) ResetPassword(c *gin.Context) {\n\tvar req resetRequest\n\n\tif valid := bindData(c, &req); !valid {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\t// Check if passwords match\n\tif req.Password != req.ConfirmPassword {\n\t\ttoFieldErrorResponse(c, \"Password\", apperrors.PasswordsDoNotMatch)\n\t\treturn\n\t}\n\n\tctx := c.Request.Context()\n\tuser, err := h.userService.ResetPassword(ctx, req.Password, req.Token)\n\n\tif err != nil {\n\t\tif err.Error() == apperrors.NewBadRequest(apperrors.InvalidResetToken).Error() {\n\t\t\ttoFieldErrorResponse(c, \"Token\", apperrors.InvalidResetToken)\n\t\t\treturn\n\t\t}\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tsetUserSession(c, user.ID)\n\n\tc.JSON(http.StatusOK, user)\n}\n"
  },
  {
    "path": "server/handler/auth_handler_test.go",
    "content": "package handler\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/sentrionic/valkyrie/service\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestHandler_Register(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tuser := fixture.GetMockUser()\n\treqUser := &model.User{\n\t\tEmail:    user.Email,\n\t\tPassword: user.Password,\n\t\tUsername: user.Username,\n\t}\n\n\tt.Run(\"Email, Username and Password Required\", func(t *testing.T) {\n\t\t// We just want this to show that it's not called in this case\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Register\", mock.AnythingOfType(\"*model.User\")).Return(nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// don't need a middleware as we don't yet have authorized user\n\t\trouter := gin.Default()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\t// create a request body with empty email and password\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    \"\",\n\t\t\t\"username\": \"\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, 400, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"Register\")\n\t})\n\n\tt.Run(\"Invalid email\", func(t *testing.T) {\n\t\t// We just want this to show that it's not called in this case\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Register\", mock.AnythingOfType(\"*model.User\")).Return(nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// don't need a middleware as we don't yet have authorized user\n\t\trouter := gin.Default()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\t// create a request body with empty email and password\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    \"bob@bob\",\n\t\t\t\"username\": \"bobby\",\n\t\t\t\"password\": \"supersecret1234\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, 400, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"Signup\")\n\t})\n\n\tt.Run(\"Username too short\", func(t *testing.T) {\n\t\t// We just want this to show that it's not called in this case\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Register\", mock.AnythingOfType(\"*model.User\")).Return(nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// don't need a middleware as we don't yet have authorized user\n\t\trouter := gin.Default()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\t// create a request body with empty email and password\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    \"bob@bob.com\",\n\t\t\t\"username\": \"bo\",\n\t\t\t\"password\": \"superpassword\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, 400, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"Register\")\n\t})\n\n\tt.Run(\"Password too short\", func(t *testing.T) {\n\t\t// We just want this to show that it's not called in this case\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Register\", mock.AnythingOfType(\"*model.User\")).Return(nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// don't need a middleware as we don't yet have authorized user\n\t\trouter := gin.Default()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\t// create a request body with empty email and password\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    \"bob@bob.com\",\n\t\t\t\"username\": \"bobby\",\n\t\t\t\"password\": \"supe\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, 400, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"Register\")\n\t})\n\n\tt.Run(\"Username too long\", func(t *testing.T) {\n\t\t// We just want this to show that it's not called in this case\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Register\", mock.AnythingOfType(\"*model.User\")).Return(nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// don't need a middleware as we don't yet have authorized user\n\t\trouter := gin.Default()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\t// create a request body with empty email and password\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    \"bob@bob.com\",\n\t\t\t\"username\": \"kjhasiudaiusdiuadiuagszuidgaiszugdziasgdiazgsdiazugdipas\",\n\t\t\t\"password\": \"superpassword\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, 400, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"Register\")\n\t})\n\n\tt.Run(\"Error returned from UserService\", func(t *testing.T) {\n\t\tu := &model.User{\n\t\t\tEmail:    reqUser.Email,\n\t\t\tUsername: reqUser.Username,\n\t\t\tPassword: reqUser.Password,\n\t\t}\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Register\", u).Return(nil, apperrors.NewConflict(\"User Already Exists\", u.Email))\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// don't need a middleware as we don't yet have authorized user\n\t\trouter := gin.Default()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\t// create a request body with empty email and password\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    u.Email,\n\t\t\t\"username\": u.Username,\n\t\t\t\"password\": u.Password,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, 409, rr.Code)\n\t\tmockUserService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Successful Creation\", func(t *testing.T) {\n\t\tu := &model.User{\n\t\t\tEmail:    reqUser.Email,\n\t\t\tUsername: reqUser.Username,\n\t\t\tPassword: reqUser.Password,\n\t\t}\n\n\t\tmockUserService := new(mocks.UserService)\n\n\t\tmockUserService.\n\t\t\tOn(\"Register\", u).\n\t\t\tReturn(reqUser, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\t// create a request body with empty email and password\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    u.Email,\n\t\t\t\"username\": u.Username,\n\t\t\t\"password\": u.Password,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/register\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(u)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusCreated, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockUserService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_Login(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\t// setup mock services, gin engine/router, handler layer\n\tmockUserService := new(mocks.UserService)\n\n\trouter := getTestRouter()\n\n\tNewHandler(&Config{\n\t\tR:           router,\n\t\tUserService: mockUserService,\n\t})\n\n\tt.Run(\"Bad request data\", func(t *testing.T) {\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// create a request body with invalid fields\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    \"notanemail\",\n\t\t\t\"password\": \"short\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/login\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"Login\")\n\t})\n\n\tt.Run(\"Error Returned from UserService.Login\", func(t *testing.T) {\n\t\temail := \"bob@bob.com\"\n\t\tpassword := \"pwdoesnotmatch123\"\n\n\t\tmockUSArgs := mock.Arguments{\n\t\t\temail, password,\n\t\t}\n\n\t\t// so we can check for a known status code\n\t\tmockError := apperrors.NewAuthorization(\"invalid email/password combo\")\n\n\t\tmockUserService.On(\"Login\", mockUSArgs...).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// create a request body with valid fields\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    email,\n\t\t\t\"password\": password,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/login\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockUserService.AssertCalled(t, \"Login\", mockUSArgs...)\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t})\n\n\tt.Run(\"Successful Login\", func(t *testing.T) {\n\t\tuser := fixture.GetMockUser()\n\n\t\tmockUSArgs := mock.Arguments{\n\t\t\tuser.Email,\n\t\t\tuser.Password,\n\t\t}\n\n\t\tmockUserService.On(\"Login\", mockUSArgs...).Return(user, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// create a request body with valid fields\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\":    user.Email,\n\t\t\t\"password\": user.Password,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/login\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(user)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockUserService.AssertCalled(t, \"Login\", mockUSArgs...)\n\t})\n}\n\nfunc TestHandler_Logout(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tuid := service.GenerateId()\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// creates a test context for setting a user\n\t\trouter := getAuthenticatedTestRouter(uid)\n\n\t\tNewHandler(&Config{\n\t\t\tR: router,\n\t\t})\n\n\t\trequest, _ := http.NewRequest(http.MethodPost, \"/api/account/logout\", nil)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(true)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\trouter.Use(func(c *gin.Context) {\n\t\t\tcontextUserId, exists := c.Get(\"userId\")\n\t\t\tassert.Equal(t, exists, false)\n\t\t\tassert.Nil(t, contextUserId)\n\n\t\t\tsession := sessions.Default(c)\n\t\t\tid := session.Get(\"userId\")\n\t\t\tassert.Nil(t, id)\n\t\t})\n\t})\n}\n\nfunc TestHandler_ForgotPassword(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\n\tt.Run(\"ForgotPassword success\", func(t *testing.T) {\n\t\trouter := gin.Default()\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"GetByEmail\", mockUser.Email).Return(mockUser, nil)\n\n\t\tForgotPasswordArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockUserService.\n\t\t\tOn(\"ForgotPassword\", ForgotPasswordArgs...).\n\t\t\tReturn(nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\": mockUser.Email,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, _ := http.NewRequest(http.MethodPost, \"/api/account/forgot-password\", bytes.NewBuffer(reqBody))\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(true)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertCalled(t, \"ForgotPassword\", ForgotPasswordArgs...)\n\t})\n\n\tt.Run(\"ForgotPassword Failure\", func(t *testing.T) {\n\t\trouter := gin.Default()\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"GetByEmail\", mockUser.Email).Return(mockUser, nil)\n\n\t\tForgotPasswordArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockUserService.\n\t\t\tOn(\"ForgotPassword\", ForgotPasswordArgs...).\n\t\t\tReturn(mockError)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\": mockUser.Email,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, _ := http.NewRequest(http.MethodPost, \"/api/account/forgot-password\", bytes.NewBuffer(reqBody))\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertCalled(t, \"ForgotPassword\", ForgotPasswordArgs...)\n\t})\n\n\tt.Run(\"No user found\", func(t *testing.T) {\n\t\trouter := gin.Default()\n\n\t\tmockUserService := new(mocks.UserService)\n\n\t\tmockError := apperrors.NewNotFound(\"email\", mockUser.Email)\n\t\tmockUserService.On(\"GetByEmail\", mockUser.Email).Return(&model.User{}, mockError)\n\n\t\tForgotPasswordArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockUserService.\n\t\t\tOn(\"ForgotPassword\", ForgotPasswordArgs...).\n\t\t\tReturn(nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"email\": mockUser.Email,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, _ := http.NewRequest(http.MethodPost, \"/api/account/forgot-password\", bytes.NewBuffer(reqBody))\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(true)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertNotCalled(t, \"ForgotPassword\", ForgotPasswordArgs...)\n\t})\n}\n\nfunc TestHandler_ForgotPassword_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\n\trouter := gin.Default()\n\n\tmockUserService := new(mocks.UserService)\n\tmockUserService.On(\"GetByEmail\", mockUser.Email).Return(mockUser, nil)\n\n\tNewHandler(&Config{\n\t\tR:            router,\n\t\tUserService:  mockUserService,\n\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t})\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody gin.H\n\t}{\n\t\t{\n\t\t\tname: \"Email required\",\n\t\t\tbody: gin.H{},\n\t\t},\n\t\t{\n\t\t\tname: \"Invalid Email\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"email\": \"invalidemail\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\treqBody, err := json.Marshal(tc.body)\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/forgot-password\", bytes.NewBuffer(reqBody))\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockUserService.AssertNotCalled(t, \"ForgotPassword\")\n\t\t})\n\t}\n}\n\nfunc TestHandler_ResetPassword(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\ttoken := fixture.RandStringRunes(18)\n\n\tt.Run(\"ResetPassword success\", func(t *testing.T) {\n\t\tmockUserService := new(mocks.UserService)\n\n\t\trouter := getTestRouter()\n\n\t\tResetPasswordArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockUser.Password,\n\t\t\ttoken,\n\t\t}\n\n\t\tmockUserService.\n\t\t\tOn(\"ResetPassword\", ResetPasswordArgs...).\n\t\t\tReturn(mockUser, nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"token\":              token,\n\t\t\t\"newPassword\":        mockUser.Password,\n\t\t\t\"confirmNewPassword\": mockUser.Password,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, _ := http.NewRequest(http.MethodPost, \"/api/account/reset-password\", bytes.NewBuffer(reqBody))\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(mockUser)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertCalled(t, \"ResetPassword\", ResetPasswordArgs...)\n\t})\n\n\tt.Run(\"ResetPassword Failure\", func(t *testing.T) {\n\t\tmockUserService := new(mocks.UserService)\n\n\t\trouter := getTestRouter()\n\n\t\tResetPasswordArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockUser.Password,\n\t\t\ttoken,\n\t\t}\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockUserService.\n\t\t\tOn(\"ResetPassword\", ResetPasswordArgs...).\n\t\t\tReturn(nil, mockError)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tUserService:  mockUserService,\n\t\t\tMaxBodyBytes: 4 * 1024 * 1024,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"token\":              token,\n\t\t\t\"newPassword\":        mockUser.Password,\n\t\t\t\"confirmNewPassword\": mockUser.Password,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, _ := http.NewRequest(http.MethodPost, \"/api/account/reset-password\", bytes.NewBuffer(reqBody))\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockUserService.AssertCalled(t, \"ResetPassword\", ResetPasswordArgs...)\n\t})\n}\n\nfunc TestHandler_ResetPassword_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\trouter := gin.Default()\n\n\tmockUserService := new(mocks.UserService)\n\n\tNewHandler(&Config{\n\t\tR:           router,\n\t\tUserService: mockUserService,\n\t})\n\n\tpassword := fixture.RandStringRunes(6)\n\tconfirmPassword := password\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody gin.H\n\t}{\n\t\t{\n\t\t\tname: \"Token required\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"newPassword\":        password,\n\t\t\t\t\"confirmNewPassword\": password,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Password required\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"token\":              fixture.RandStringRunes(18),\n\t\t\t\t\"confirmNewPassword\": password,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Password too short\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"token\":              fixture.RandStringRunes(18),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(5),\n\t\t\t\t\"confirmNewPassword\": confirmPassword,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"NewPassword too long\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"token\":              fixture.RandStringRunes(16),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(151),\n\t\t\t\t\"confirmNewPassword\": confirmPassword,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ConfirmNewPassword too short\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"token\":              fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(6),\n\t\t\t\t\"confirmNewPassword\": fixture.RandStringRunes(5),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ConfirmNewPassword too long\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"currentPassword\":    fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(6),\n\t\t\t\t\"confirmNewPassword\": fixture.RandStringRunes(151),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ConfirmNewPassword required\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"token\":       fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\": fixture.RandStringRunes(6),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"NewPassword and ConfirmNewPassword not equal\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"token\":              fixture.RandStringRunes(6),\n\t\t\t\t\"newPassword\":        fixture.RandStringRunes(6),\n\t\t\t\t\"confirmNewPassword\": fixture.RandStringRunes(6),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\treqBody, err := json.Marshal(tc.body)\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/account/reset-password\", bytes.NewBuffer(reqBody))\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockUserService.AssertNotCalled(t, \"ResetPassword\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "server/handler/bind_data.go",
    "content": "package handler\n\nimport (\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// Request contains the validate function which validates the request with bindData\ntype Request interface {\n\tvalidate() error\n}\n\n// bindData is helper function, returns false if data is not bound\nfunc bindData(c *gin.Context, req Request) bool {\n\t// Bind incoming json to struct and check for validation errors\n\tif err := c.ShouldBind(req); err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn false\n\t}\n\n\tif err := req.validate(); err != nil {\n\t\terrors := strings.Split(err.Error(), \";\")\n\t\tfErrors := make([]model.FieldError, 0)\n\n\t\tfor _, e := range errors {\n\t\t\tsplit := strings.Split(e, \":\")\n\t\t\ter := model.FieldError{\n\t\t\t\tField:   strings.TrimSpace(split[0]),\n\t\t\t\tMessage: strings.TrimSpace(split[1]),\n\t\t\t}\n\t\t\tfErrors = append(fErrors, er)\n\t\t}\n\n\t\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\t\"errors\": fErrors,\n\t\t})\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "server/handler/channel_handler.go",
    "content": "package handler\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation/v4\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n/*\n * ChannelHandler contains all routes related to channel actions (/api/channels)\n */\n\n// GuildChannels returns the given guild's channels\n// GuildChannels godoc\n// @Tags Channels\n// @Summary Get Guild Channels\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {array} model.ChannelResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /channels/{guildId} [get]\nfunc (h *Handler) GuildChannels(c *gin.Context) {\n\tguildId := c.Param(\"id\")\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Only get the channels if the user is a member\n\tif !isMember(guild, userId) {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tchannels, err := h.channelService.GetChannels(userId, guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find channels for guild id: %v\\n%v\", guildId, err)\n\t\te := apperrors.NewNotFound(\"channels\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, channels)\n}\n\n// channelReq specifies the input form for creating a channel\n// IsPublic and Members do not need to be specified if you want\n// to create a public channel\ntype channelReq struct {\n\t// Channel Name. 3 to 30 character\n\tName string `json:\"name\"`\n\t// Default is true\n\tIsPublic *bool `json:\"isPublic\"`\n\t// Array of memberIds\n\tMembers []string `json:\"members\"`\n} //@name ChannelRequest\n\nfunc (r channelReq) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Name, validation.Required, validation.Length(3, 30)),\n\t)\n}\n\nfunc (r *channelReq) sanitize() {\n\tr.Name = strings.TrimSpace(r.Name)\n}\n\n// CreateChannel creates a channel for the given guild param\n// CreateChannel godoc\n// @Tags Channels\n// @Summary Create Channel\n// @Accepts json\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {array} model.ChannelResponse\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /channels/{guildId} [post]\nfunc (h *Handler) CreateChannel(c *gin.Context) {\n\tvar req channelReq\n\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tuserId := c.MustGet(\"userId\").(string)\n\tguildId := c.Param(\"id\")\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the server already has 50 channels\n\tif len(guild.Channels) >= model.MaximumChannels {\n\t\te := apperrors.NewBadRequest(apperrors.ChannelLimitError)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tchannelParams := model.Channel{\n\t\tName:     req.Name,\n\t\tIsPublic: true,\n\t\tGuildID:  &guildId,\n\t}\n\n\t// Channel is private\n\tif req.IsPublic != nil && !*req.IsPublic {\n\t\tchannelParams.IsPublic = false\n\n\t\t// Add the current user to the members if they are not in there\n\t\tif !containsUser(req.Members, userId) {\n\t\t\treq.Members = append(req.Members, userId)\n\t\t}\n\t\tmembers, err := h.guildService.FindUsersByIds(req.Members, guildId)\n\n\t\tif err != nil {\n\t\t\te := apperrors.NewInternal()\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\t// Create private channel members\n\t\tchannelParams.PCMembers = append(channelParams.PCMembers, *members...)\n\t}\n\n\tchannel, err := h.channelService.CreateChannel(&channelParams)\n\n\tif err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tguild.Channels = append(guild.Channels, *channel)\n\n\tif err = h.guildService.UpdateGuild(guild); err != nil {\n\t\tlog.Printf(\"Failed to update guild: %v\\n\", err.Error())\n\t\te := apperrors.NewInternal()\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tresponse := channel.SerializeChannel()\n\n\t// Emit the new channel to the guild members\n\tif channel.IsPublic {\n\t\th.socketService.EmitNewChannel(guildId, &response)\n\t\t// Emit to private channel members\n\t} else {\n\t\th.socketService.EmitNewPrivateChannel(req.Members, &response)\n\t}\n\n\tc.JSON(http.StatusCreated, response)\n}\n\n// PrivateChannelMembers returns the ids of all members\n// that are part of the channel\n// PrivateChannelMembers godoc\n// @Tags Channels\n// @Summary Get Members of the given Channel\n// @Produce  json\n// @Param channelId path string true \"Channel ID\"\n// @Success 200 {array} string\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /channels/{channelId}/members [get]\nfunc (h *Handler) PrivateChannelMembers(c *gin.Context) {\n\tchannelId := c.Param(\"id\")\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tchannel, err := h.channelService.Get(channelId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"channel\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif channel.GuildID == nil {\n\t\te := apperrors.NewNotFound(\"channel\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tguild, err := h.guildService.GetGuild(*channel.GuildID)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"channel\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Public channels do not have any private members\n\tif channel.IsPublic {\n\t\tvar empty = make([]string, 0)\n\t\tc.JSON(http.StatusOK, empty)\n\t\treturn\n\t}\n\n\tmembers, err := h.channelService.GetPrivateChannelMembers(channelId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find members for channel: %v\\n%v\", channelId, err)\n\t\te := apperrors.NewNotFound(\"members\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, members)\n}\n\n// DirectMessages returns a list of the current users DMs\n// DirectMessages godoc\n// @Tags Channels\n// @Summary Get User's DMs\n// @Produce  json\n// @Success 200 {array} model.DirectMessage\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /channels/me/dm [get]\nfunc (h *Handler) DirectMessages(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tchannels, err := h.channelService.GetDirectMessages(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find dms for user id: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"dms\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// If the user does not have any dms, return an empty array\n\tif len(*channels) == 0 {\n\t\tvar empty = make([]model.DirectMessage, 0)\n\t\tc.JSON(http.StatusOK, empty)\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, channels)\n}\n\n// GetOrCreateDM gets the DM with the given member and creates it\n// if it does not already exist\n// DirectMessages godoc\n// @Tags Channels\n// @Summary Get or Create DM\n// @Produce  json\n// @Param channelId path string true \"Member ID\"\n// @Success 200 {object} model.DirectMessage\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /channels/{channelId}/dm [post]\nfunc (h *Handler) GetOrCreateDM(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tmemberId := c.Param(\"id\")\n\n\tif userId == memberId {\n\t\te := apperrors.NewBadRequest(apperrors.DMYourselfError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmember, err := h.friendService.GetMemberById(memberId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find member for id: %v\\n%v\", memberId, err)\n\t\te := apperrors.NewNotFound(\"member\", memberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// check if dm channel already exists with these members\n\tdmId, err := h.channelService.GetDirectMessageChannel(userId, memberId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find or create dms for user id: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"dms\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// dm already exists\n\tif dmId != nil && *dmId != \"\" {\n\t\t_ = h.channelService.SetDirectMessageStatus(*dmId, userId, true)\n\t\tc.JSON(http.StatusOK, toDMChannel(member, *dmId, userId))\n\t\treturn\n\t}\n\n\t// Create the dm channel between the current user and the member\n\tid := fmt.Sprintf(\"%s-%s\", userId, memberId)\n\tchannelParams := model.Channel{\n\t\tName:     id,\n\t\tIsPublic: false,\n\t\tIsDM:     true,\n\t}\n\n\tchannel, err := h.channelService.CreateChannel(&channelParams)\n\n\t// Create the DM channel\n\tif err != nil {\n\t\tlog.Printf(\"Failed to create channel: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Add the users to it\n\tids := []string{userId, memberId}\n\terr = h.channelService.AddDMChannelMembers(ids, channel.ID, userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to create channel: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, toDMChannel(member, channel.ID, userId))\n}\n\n// toDMChannel returns the DM response for the given channel and member\nfunc toDMChannel(member *model.User, channelId string, userId string) model.DirectMessage {\n\treturn model.DirectMessage{\n\t\tId: channelId,\n\t\tUser: model.DMUser{\n\t\t\tId:       member.ID,\n\t\t\tUsername: member.Username,\n\t\t\tImage:    member.Image,\n\t\t\tIsOnline: member.IsOnline,\n\t\t\tIsFriend: isFriend(member, userId),\n\t\t},\n\t}\n}\n\n// EditChannel edits the specified channel\n// EditChannel godoc\n// @Tags Channels\n// @Summary Edit Channel\n// @Accepts json\n// @Produce  json\n// @Param channelId path string true \"Channel ID\"\n// @Param request body channelReq true \"Edit Channel\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /channels/{channelId} [put]\nfunc (h *Handler) EditChannel(c *gin.Context) {\n\tvar req channelReq\n\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tuserId := c.MustGet(\"userId\").(string)\n\tchannelId := c.Param(\"id\")\n\n\tchannel, err := h.channelService.Get(channelId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"channel\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tguild, err := h.guildService.GetGuild(*channel.GuildID)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", *channel.GuildID)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tisPublic := true\n\tif req.IsPublic != nil {\n\t\tisPublic = *req.IsPublic\n\t}\n\n\t// Used to be private and now is public\n\tif isPublic && !channel.IsPublic {\n\t\terr = h.channelService.CleanPCMembers(channelId)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"error removing pc members: %v\", err)\n\t\t}\n\t}\n\n\tchannel.IsPublic = isPublic\n\tchannel.Name = req.Name\n\n\t// Member Changes\n\tif !isPublic {\n\t\t// Check if the array contains the current member\n\t\tif !containsUser(req.Members, userId) {\n\t\t\treq.Members = append(req.Members, userId)\n\t\t}\n\n\t\t// Current members of the channel\n\t\tcurrent := make([]string, 0)\n\t\tfor _, member := range channel.PCMembers {\n\t\t\tcurrent = append(current, member.ID)\n\t\t}\n\n\t\t// Newly added members\n\t\tnewMembers := difference(req.Members, current)\n\t\t// Members that got removed\n\t\ttoRemove := difference(current, req.Members)\n\n\t\terr = h.channelService.AddPrivateChannelMembers(newMembers, channelId)\n\t\tif err != nil {\n\t\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\t\"error\": err,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\terr = h.channelService.RemovePrivateChannelMembers(toRemove, channelId)\n\t\tif err != nil {\n\t\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\t\"error\": err,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err = h.channelService.UpdateChannel(channel); err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit the channel changes to the guild members\n\tresponse := channel.SerializeChannel()\n\th.socketService.EmitEditChannel(*channel.GuildID, &response)\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// difference returns the elements in `a` that aren't in `b`.\nfunc difference(a, b []string) []string {\n\tmb := make(map[string]struct{}, len(b))\n\tfor _, x := range b {\n\t\tmb[x] = struct{}{}\n\t}\n\tvar diff []string\n\tfor _, x := range a {\n\t\tif _, found := mb[x]; !found {\n\t\t\tdiff = append(diff, x)\n\t\t}\n\t}\n\treturn diff\n}\n\n// DeleteChannel removes the given channel from the guild\n// DeleteChannel godoc\n// @Tags Channels\n// @Summary Delete Channel\n// @Produce  json\n// @Param id path string true \"Channel ID\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /channels/{id} [delete]\nfunc (h *Handler) DeleteChannel(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tchannelId := c.Param(\"id\")\n\n\tchannel, err := h.channelService.Get(channelId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"channel\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tguild, err := h.guildService.GetGuild(*channel.GuildID)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"channel\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the guild has the minimum amount of channels\n\tif len(guild.Channels) <= model.MinimumChannels {\n\t\te := apperrors.NewBadRequest(apperrors.OneChannelRequired)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif err = h.channelService.DeleteChannel(channel); err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit signal to remove the channel from the guild\n\th.socketService.EmitDeleteChannel(channel)\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// CloseDM closes the DM on the current users side\n// CloseDM godoc\n// @Tags Channels\n// @Summary Close DM\n// @Produce  json\n// @Param id path string true \"DM Channel ID\"\n// @Success 200 {object} model.Success\n// @Failure 404 {object} model.ErrorResponse\n// @Router /channels/{id}/dm [delete]\nfunc (h *Handler) CloseDM(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tchannelId := c.Param(\"id\")\n\n\tdmId, err := h.channelService.GetDMByUserAndChannel(userId, channelId)\n\n\tif err != nil || dmId == \"\" {\n\t\tlog.Printf(\"Unable to find or create dms for user id: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"dms\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t_ = h.channelService.SetDirectMessageStatus(channelId, userId, false)\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// containsUser checks if the array contains the user\nfunc containsUser(members []string, userId string) bool {\n\tfor _, m := range members {\n\t\tif m == userId {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/handler/channel_handler_test.go",
    "content": "package handler\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestHandler_GuildChannels(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\n\t\tresponse := make([]model.ChannelResponse, 0)\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\t\tresponse = append(response, mockChannel.SerializeChannel())\n\t\t}\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"GetChannels\", authUser.ID, mockGuild.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(response)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"guild\", id)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", id).Return(nil, mockError)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", id)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", id)\n\t\tmockChannelService.AssertNotCalled(t, \"GetChannels\")\n\t})\n\n\tt.Run(\"Not a member of the guild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockError := apperrors.NewNotFound(\"guild\", mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockChannelService.AssertNotCalled(t, \"GetChannels\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"GetChannels\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\t\tmockError := apperrors.NewNotFound(\"channels\", mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"GetChannels\", authUser.ID, mockGuild.ID).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_CreateChannel(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful channel creation\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tchannelParams := &model.Channel{\n\t\t\tName:     mockChannel.Name,\n\t\t\tIsPublic: true,\n\t\t\tGuildID:  &mockGuild.ID,\n\t\t}\n\t\tmockChannelService.On(\"CreateChannel\", channelParams).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tresponse := mockChannel.SerializeChannel()\n\t\tmockSocketService.On(\"EmitNewChannel\", mockGuild.ID, &response)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(response)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusCreated, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", mockGuild.ID)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(nil, mockError)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewChannel\")\n\t})\n\n\tt.Run(\"Guild already has the maximum number of channels\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tfor i := 0; i < model.MaximumChannels; i++ {\n\t\t\tchannel := fixture.GetMockChannel(mockGuild.ID)\n\t\t\tmockGuild.Channels = append(mockGuild.Channels, *channel)\n\t\t}\n\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.ChannelLimitError)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewChannel\")\n\t})\n\n\tt.Run(\"Not the guild owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewChannel\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewChannel\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tchannelParams := &model.Channel{\n\t\t\tName:     mockChannel.Name,\n\t\t\tIsPublic: true,\n\t\t\tGuildID:  &mockGuild.ID,\n\t\t}\n\t\tmockError := apperrors.NewInternal()\n\t\tmockChannelService.On(\"CreateChannel\", channelParams).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewChannel\")\n\t})\n\n\tt.Run(\"Successful private channel creation\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmembers := make([]model.User, 0)\n\t\tmembers = append(members, *authUser)\n\n\t\treqMembers := []string{authUser.ID}\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(nil)\n\t\tmockGuildService.On(\"FindUsersByIds\", reqMembers, mockGuild.ID).Return(&members, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tchannelParams := &model.Channel{\n\t\t\tName:     mockChannel.Name,\n\t\t\tIsPublic: false,\n\t\t\tGuildID:  &mockGuild.ID,\n\t\t}\n\n\t\tchannelParams.PCMembers = append(channelParams.PCMembers, *authUser)\n\t\tmockChannelService.On(\"CreateChannel\", channelParams).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tresponse := mockChannel.SerializeChannel()\n\t\tmockSocketService.On(\"EmitNewChannel\", mockGuild.ID, &response)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\":     mockChannel.Name,\n\t\t\t\"isPublic\": false,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(response)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusCreated, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_CreateChannel_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\trouter := getAuthenticatedTestRouter(mockUser.ID)\n\n\tmockGuildService := new(mocks.GuildService)\n\tmockChannelService := new(mocks.ChannelService)\n\n\tNewHandler(&Config{\n\t\tR:              router,\n\t\tGuildService:   mockGuildService,\n\t\tChannelService: mockChannelService,\n\t})\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody gin.H\n\t}{\n\t\t{\n\t\t\tname: \"Name required\",\n\t\t\tbody: gin.H{},\n\t\t},\n\t\t{\n\t\t\tname: \"Name too short\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"name\": fixture.RandStr(2),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Name too long\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"name\": fixture.RandStr(31),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\treqBody, err := json.Marshal(tc.body)\n\t\t\tassert.NoError(t, err)\n\n\t\t\turl := fmt.Sprintf(\"/api/channels/%s\", fixture.RandID())\n\t\t\trequest, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(reqBody))\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t\t})\n\t}\n}\n\nfunc TestHandler_PrivateChannelMembers(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockChannel.IsPublic = false\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tresponse := make([]string, 0)\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tmockUser := fixture.GetMockUser()\n\t\t\tresponse = append(response, mockUser.ID)\n\t\t}\n\n\t\tmockChannelService.On(\"GetPrivateChannelMembers\", mockChannel.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s/members\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(response)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(\"\")\n\t\tmockChannel.IsPublic = false\n\t\tmockError := apperrors.NewNotFound(\"channel\", mockChannel.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s/members\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"GetPrivateChannelMembers\")\n\t})\n\n\tt.Run(\"Channel not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"channel\", id)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", id).Return(nil, mockError)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s/members\", id)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"GetPrivateChannelMembers\")\n\t})\n\n\tt.Run(\"Not the guild owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockChannel.IsPublic = false\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s/members\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\tassert.NoError(t, err)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, e.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockChannelService.AssertNotCalled(t, \"GetPrivateChannelMembers\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockChannel.IsPublic = false\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s/members\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertNotCalled(t, \"Get\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"GetPrivateChannelMembers\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockChannel.IsPublic = false\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockError := apperrors.NewNotFound(\"members\", mockChannel.ID)\n\t\tmockChannelService.On(\"GetPrivateChannelMembers\", mockChannel.ID).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/channels/%s/members\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_DirectMessages(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tresponse := make([]model.DirectMessage, 0)\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tuser := fixture.GetMockUser()\n\t\t\tresponse = append(response, toDMChannel(user, fixture.RandID(), authUser.ID))\n\t\t}\n\n\t\tmockChannelService.On(\"GetDirectMessages\", authUser.ID).Return(&response, nil)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/channels/me/dm\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(response)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/channels/me/dm\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertNotCalled(t, \"GetDirectMessages\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tmockError := apperrors.NewNotFound(\"dms\", authUser.ID)\n\t\tmockChannelService.On(\"GetDirectMessages\", authUser.ID).Return(nil, mockError)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/channels/me/dm\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_GetOrCreateDM(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully returned an already existing DM\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tdmId := fixture.RandID()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"GetDirectMessageChannel\", authUser.ID, mockUser.ID).Return(&dmId, nil)\n\t\tmockChannelService.On(\"SetDirectMessageStatus\", dmId, authUser.ID, true).Return(nil)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tFriendService:  mockFriendService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(toDMChannel(mockUser, dmId, authUser.ID))\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t})\n\n\tt.Run(\"Successfully returned a new DM\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"GetDirectMessageChannel\", authUser.ID, mockUser.ID).Return(nil, nil)\n\n\t\tmockDM := fixture.GetMockDMChannel()\n\t\tchannelParams := &model.Channel{\n\t\t\tName:     fmt.Sprintf(\"%s-%s\", authUser.ID, mockUser.ID),\n\t\t\tIsPublic: false,\n\t\t\tIsDM:     true,\n\t\t}\n\t\tmockChannelService.On(\"CreateChannel\", channelParams).Return(mockDM, nil)\n\n\t\tids := []string{authUser.ID, mockUser.ID}\n\t\tmockArgs := mock.Arguments{\n\t\t\tids,\n\t\t\tmockDM.ID,\n\t\t\tauthUser.ID,\n\t\t}\n\t\tmockChannelService.On(\"AddDMChannelMembers\", mockArgs...).Return(nil)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tFriendService:  mockFriendService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(toDMChannel(mockUser, mockDM.ID, authUser.ID))\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Member not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"member\", id)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", id).Return(nil, mockError)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tFriendService:  mockFriendService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", id)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertCalled(t, \"GetMemberById\", id)\n\t\tmockChannelService.AssertNotCalled(t, \"GetDirectMessageChannel\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t})\n\n\tt.Run(\"Member and AuthUser are the same\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tFriendService:  mockFriendService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", authUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\te := apperrors.NewBadRequest(apperrors.DMYourselfError)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, e.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetMemberById\")\n\t\tmockChannelService.AssertNotCalled(t, \"GetDirectMessageChannel\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tFriendService:  mockFriendService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", fixture.RandID())\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetMemberById\")\n\t\tmockChannelService.AssertNotCalled(t, \"GetDirectMessageChannel\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"GetDirectMessageChannel\", authUser.ID, mockUser.ID).Return(nil, nil)\n\n\t\tmockDM := fixture.GetMockDMChannel()\n\t\tchannelParams := &model.Channel{\n\t\t\tName:     fmt.Sprintf(\"%s-%s\", authUser.ID, mockUser.ID),\n\t\t\tIsPublic: false,\n\t\t\tIsDM:     true,\n\t\t}\n\t\tmockChannelService.On(\"CreateChannel\", channelParams).Return(mockDM, nil)\n\n\t\tids := []string{authUser.ID, mockUser.ID}\n\t\tmockArgs := mock.Arguments{\n\t\t\tids,\n\t\t\tmockDM.ID,\n\t\t\tauthUser.ID,\n\t\t}\n\t\tmockError := apperrors.NewInternal()\n\t\tmockChannelService.On(\"AddDMChannelMembers\", mockArgs...).Return(mockError)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tFriendService:  mockFriendService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_EditChannel(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful edited channel\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockChannelService.On(\"UpdateChannel\", mockChannel).Return(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tresponse := mockChannel.SerializeChannel()\n\t\tmockSocketService.On(\"EmitEditChannel\", mockGuild.ID, &response)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockError := apperrors.NewNotFound(\"guild\", mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(nil, mockError)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockChannelService.AssertNotCalled(t, \"UpdateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditChannel\")\n\t})\n\n\tt.Run(\"Channel not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"channel\", id)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", id).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": fixture.RandStr(8),\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", id)\n\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"UpdateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditChannel\")\n\t})\n\n\tt.Run(\"Not the guild owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, e.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockChannelService.AssertNotCalled(t, \"UpdateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditChannel\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": fixture.RandStr(8),\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", id)\n\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertNotCalled(t, \"Get\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"UpdateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditChannel\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockChannelService.On(\"UpdateChannel\", mockChannel).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tresponse := mockChannel.SerializeChannel()\n\t\tmockSocketService.On(\"EmitEditChannel\", mockGuild.ID, &response)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockChannel.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditChannel\")\n\t})\n\n\tt.Run(\"Private channel made public\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockChannel.IsPublic = false\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tresponse := mockChannel.SerializeChannel()\n\n\t\tmockChannelService.On(\"CleanPCMembers\", mockChannel.ID).Return(nil)\n\t\tmockChannelService.On(\"UpdateChannel\", mockChannel).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockChannel.IsPublic = true\n\t\t\t\tresponse = mockChannel.SerializeChannel()\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitEditChannel\", mockGuild.ID, &response)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\":     mockChannel.Name,\n\t\t\t\"isPublic\": true,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Channel made private\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"AddPrivateChannelMembers\", []string{authUser.ID}, mockChannel.ID).Return(nil)\n\t\tmockChannelService.On(\"RemovePrivateChannelMembers\", []string(nil), mockChannel.ID).Return(nil)\n\n\t\tresponse := mockChannel.SerializeChannel()\n\t\tmockChannelService.On(\"UpdateChannel\", mockChannel).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockChannel.IsPublic = false\n\t\t\t\tresponse = mockChannel.SerializeChannel()\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitEditChannel\", mockGuild.ID, &response)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\":     mockChannel.Name,\n\t\t\t\"isPublic\": false,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_EditChannel_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\trouter := getAuthenticatedTestRouter(mockUser.ID)\n\n\tmockGuildService := new(mocks.GuildService)\n\tmockChannelService := new(mocks.ChannelService)\n\n\tNewHandler(&Config{\n\t\tR:              router,\n\t\tGuildService:   mockGuildService,\n\t\tChannelService: mockChannelService,\n\t})\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody gin.H\n\t}{\n\t\t{\n\t\t\tname: \"Name required\",\n\t\t\tbody: gin.H{},\n\t\t},\n\t\t{\n\t\t\tname: \"Name too short\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"name\": fixture.RandStr(2),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Name too long\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"name\": fixture.RandStr(31),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\treqBody, err := json.Marshal(tc.body)\n\t\t\tassert.NoError(t, err)\n\n\t\t\turl := fmt.Sprintf(\"/api/channels/%s\", fixture.RandID())\n\t\t\trequest, err := http.NewRequest(http.MethodPut, url, bytes.NewBuffer(reqBody))\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t\t})\n\t}\n}\n\nfunc TestHandler_DeleteChannel(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully deleted\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockGuild.Channels = append(mockGuild.Channels, *mockChannel)\n\t\tmockGuild.Channels = append(mockGuild.Channels, *fixture.GetMockChannel(mockGuild.ID))\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"DeleteChannel\", mockChannel).Return(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitDeleteChannel\", mockChannel)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Channel not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"channel\", id)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", id).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", id)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"DeleteChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteChannel\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(fixture.RandID())\n\t\tmockError := apperrors.NewNotFound(\"channel\", mockChannel.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", *mockChannel.GuildID).Return(nil, mockError)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", *mockChannel.GuildID)\n\t\tmockGuildService.AssertNotCalled(t, \"DeleteChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteChannel\")\n\t})\n\n\tt.Run(\"Not the guild owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", *mockChannel.GuildID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, e.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", *mockChannel.GuildID)\n\t\tmockGuildService.AssertNotCalled(t, \"DeleteChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteChannel\")\n\t})\n\n\tt.Run(\"Channel is last channel of the guild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockGuild.Channels = append(mockGuild.Channels, *mockChannel)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", *mockChannel.GuildID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.OneChannelRequired)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", *mockChannel.GuildID)\n\t\tmockGuildService.AssertNotCalled(t, \"DeleteChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteChannel\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", id)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertNotCalled(t, \"Get\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"DeleteChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteChannel\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockGuild.Channels = append(mockGuild.Channels, *mockChannel)\n\t\tmockGuild.Channels = append(mockGuild.Channels, *fixture.GetMockChannel(mockGuild.ID))\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockChannelService.On(\"DeleteChannel\", mockChannel).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s\", mockChannel.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_CloseDM(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully close\", func(t *testing.T) {\n\t\tchannelId := fixture.RandID()\n\t\tdmId := fixture.RandID()\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tfindArgs := mock.Arguments{\n\t\t\tauthUser.ID,\n\t\t\tchannelId,\n\t\t}\n\t\tmockChannelService.On(\"GetDMByUserAndChannel\", findArgs...).Return(dmId, nil)\n\n\t\tsetArgs := mock.Arguments{\n\t\t\tchannelId,\n\t\t\tauthUser.ID,\n\t\t\tfalse,\n\t\t}\n\t\tmockChannelService.On(\"SetDirectMessageStatus\", setArgs...).Return(nil)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", channelId)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"DM not found\", func(t *testing.T) {\n\t\tchannelId := fixture.RandID()\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tfindArgs := mock.Arguments{\n\t\t\tauthUser.ID,\n\t\t\tchannelId,\n\t\t}\n\t\tmockError := apperrors.NewNotFound(\"dms\", authUser.ID)\n\t\tmockChannelService.On(\"GetDMByUserAndChannel\", findArgs...).Return(\"\", mockError)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", channelId)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"GetDMByUserAndChannel\", findArgs...)\n\t\tmockChannelService.AssertNotCalled(t, \"SetDirectMessageStatus\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tchannelId := fixture.RandID()\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", channelId)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertNotCalled(t, \"GetDMByUserAndChannel\")\n\t\tmockChannelService.AssertNotCalled(t, \"SetDirectMessageStatus\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tchannelId := fixture.RandID()\n\t\tdmId := fixture.RandID()\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tfindArgs := mock.Arguments{\n\t\t\tauthUser.ID,\n\t\t\tchannelId,\n\t\t}\n\t\tmockChannelService.On(\"GetDMByUserAndChannel\", findArgs...).Return(dmId, nil)\n\n\t\tsetArgs := mock.Arguments{\n\t\t\tchannelId,\n\t\t\tauthUser.ID,\n\t\t\tfalse,\n\t\t}\n\t\tmockError := apperrors.NewInternal()\n\t\tmockChannelService.On(\"SetDirectMessageStatus\", setArgs...).Return(mockError)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\turl := fmt.Sprintf(\"/api/channels/%s/dm\", channelId)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t})\n}\n"
  },
  {
    "path": "server/handler/friend_handler.go",
    "content": "package handler\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"net/http\"\n)\n\n/*\n * FriendHandler contains all routes related to friend actions (/api/account)\n */\n\n// GetUserFriends returns the current users friends\n// GetUserFriends godoc\n// @Tags Friends\n// @Summary Get Current User's Friends\n// @Produce  json\n// @Success 200 {array} model.Friend\n// @Failure 404 {object} model.ErrorResponse\n// @Router /account/me/friends [get]\nfunc (h *Handler) GetUserFriends(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tfriends, err := h.friendService.GetFriends(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find friends for id: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, friends)\n}\n\n// GetUserRequests returns the current users friend requests\n// GetUserRequests godoc\n// @Tags Friends\n// @Summary Get Current User's Friend Requests\n// @Produce  json\n// @Success 200 {array} model.FriendRequest\n// @Failure 404 {object} model.ErrorResponse\n// @Router /account/me/pending [get]\nfunc (h *Handler) GetUserRequests(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\n\trequests, err := h.friendService.GetRequests(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find requests for id: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, requests)\n}\n\n// SendFriendRequest sends a friend request to the given member param\n// SendFriendRequest godoc\n// @Tags Friends\n// @Summary Send Friend Request\n// @Produce  json\n// @Param memberId path string true \"User ID\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /account/{memberId}/friend [post]\nfunc (h *Handler) SendFriendRequest(c *gin.Context) {\n\n\tuserId := c.MustGet(\"userId\").(string)\n\tmemberId := c.Param(\"memberId\")\n\n\tif userId == memberId {\n\t\te := apperrors.NewBadRequest(apperrors.AddYourselfError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tauthUser, err := h.friendService.GetMemberById(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmember, err := h.friendService.GetMemberById(memberId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", memberId, err)\n\t\te := apperrors.NewNotFound(\"user\", memberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if they are already friends and no request exists\n\tif !isFriend(authUser, member.ID) && !containsRequest(authUser, member) {\n\t\tauthUser.Requests = append(authUser.Requests, *member)\n\t\terr = h.friendService.SaveRequests(authUser)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Unable to add user as friend: %v\\n%v\", memberId, err)\n\t\t\te := apperrors.NewBadRequest(apperrors.UnableAddError)\n\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\t// Emit friends request to the added user\n\t\th.socketService.EmitAddFriendRequest(memberId, &model.FriendRequest{\n\t\t\tId:       authUser.ID,\n\t\t\tUsername: authUser.Username,\n\t\t\tImage:    authUser.Image,\n\t\t\tType:     model.Incoming,\n\t\t})\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// RemoveFriend removes the given member param from the current\n// users friends.\n// RemoveFriend godoc\n// @Tags Friends\n// @Summary Remove Friend\n// @Produce  json\n// @Param memberId path string true \"User ID\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /account/{memberId}/friend [delete]\nfunc (h *Handler) RemoveFriend(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tmemberId := c.Param(\"memberId\")\n\n\tif userId == memberId {\n\t\te := apperrors.NewBadRequest(apperrors.RemoveYourselfError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tauthUser, err := h.friendService.GetMemberById(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", memberId, err)\n\t\te := apperrors.NewNotFound(\"user\", memberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmember, err := h.friendService.GetMemberById(memberId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", memberId, err)\n\t\te := apperrors.NewNotFound(\"user\", memberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif isFriend(authUser, member.ID) {\n\t\terr = h.friendService.RemoveFriend(member.ID, authUser.ID)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Unable to remove user from friends: %v\\n%v\", memberId, err)\n\t\t\te := apperrors.NewBadRequest(apperrors.UnableRemoveError)\n\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\t// Emit signal to remove the person from the friends\n\t\th.socketService.EmitRemoveFriend(userId, memberId)\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// AcceptFriendRequest accepts the friend request from the given member param\n// AcceptFriendRequest godoc\n// @Tags Friends\n// @Summary Accept Friend's Request\n// @Produce  json\n// @Param memberId path string true \"User ID\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /account/{memberId}/friend/accept [post]\nfunc (h *Handler) AcceptFriendRequest(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tmemberId := c.Param(\"memberId\")\n\n\tif userId == memberId {\n\t\te := apperrors.NewBadRequest(apperrors.AcceptYourselfError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tauthUser, err := h.friendService.GetMemberById(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmember, err := h.friendService.GetMemberById(memberId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", memberId, err)\n\t\te := apperrors.NewNotFound(\"user\", memberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the current user is in the members requests\n\tif containsRequest(member, authUser) {\n\t\t// Add each other to friends\n\t\tauthUser.Friends = append(authUser.Friends, *member)\n\t\tmember.Friends = append(member.Friends, *authUser)\n\t\terr = h.friendService.SaveRequests(member)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Unable to accept friends request from user: %v\\n%v\", memberId, err)\n\t\t\te := apperrors.NewBadRequest(apperrors.UnableAcceptError)\n\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\terr = h.friendService.SaveRequests(authUser)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Unable to accept friends request from user: %v\\n%v\", memberId, err)\n\t\t\te := apperrors.NewBadRequest(apperrors.UnableAcceptError)\n\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\terr = h.friendService.DeleteRequest(authUser.ID, member.ID)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Unable to remove user from friends: %v\\n%v\", memberId, err)\n\t\t\te := apperrors.NewBadRequest(apperrors.UnableRemoveError)\n\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\t// Emit friend information to the accepted person\n\t\th.socketService.EmitAddFriend(authUser, member)\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// CancelFriendRequest removes the given member param from the current\n// users requests.\n// CancelFriendRequest godoc\n// @Tags Friends\n// @Summary Cancel Friend's Request\n// @Produce  json\n// @Param memberId path string true \"User ID\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /account/{memberId}/friend/cancel [post]\nfunc (h *Handler) CancelFriendRequest(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tmemberId := c.Param(\"memberId\")\n\n\tif userId == memberId {\n\t\te := apperrors.NewBadRequest(apperrors.CancelYourselfError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tauthUser, err := h.friendService.GetMemberById(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", memberId, err)\n\t\te := apperrors.NewNotFound(\"user\", memberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmember, err := h.friendService.GetMemberById(memberId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", memberId, err)\n\t\te := apperrors.NewNotFound(\"user\", memberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the member is in the current user's requests\n\tif containsRequest(authUser, member) || containsRequest(member, authUser) {\n\t\terr := h.friendService.DeleteRequest(member.ID, authUser.ID)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Unable to remove user from friends: %v\\n%v\", memberId, err)\n\t\t\te := apperrors.NewBadRequest(apperrors.UnableRemoveError)\n\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// isFriend checks if the given users are friends\nfunc isFriend(user *model.User, userId string) bool {\n\tfor _, v := range user.Friends {\n\t\tif v.ID == userId {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// containsRequest checks if the given user has a friends request from the current one\nfunc containsRequest(user *model.User, current *model.User) bool {\n\tfor _, v := range user.Requests {\n\t\tif v.ID == current.ID {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/handler/friend_handler_test.go",
    "content": "package handler\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestHandler_GetUserFriends(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tfriends := make([]model.Friend, 0)\n\n\tfor i := 0; i < 5; i++ {\n\t\tmockFriend := fixture.GetMockUser()\n\t\tfriends = append(friends, model.Friend{\n\t\t\tId:       mockFriend.ID,\n\t\t\tUsername: mockFriend.Username,\n\t\t\tImage:    mockFriend.Image,\n\t\t\tIsOnline: false,\n\t\t})\n\t}\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetFriends\", authUser.ID).Return(&friends, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account/me/friends\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(friends)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetFriends\", authUser.ID).Return(&friends, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account/me/friends\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetFriends\", authUser.ID, \"\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetFriends\", authUser.ID).Return(nil, fmt.Errorf(\"some error down call chain\"))\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account/me/friends\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", authUser.ID)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_GetUserRequests(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\trequests := make([]model.FriendRequest, 0)\n\n\tfor i := 0; i < 5; i++ {\n\t\tmockRequest := fixture.GetMockUser()\n\t\trequests = append(requests, model.FriendRequest{\n\t\t\tId:       mockRequest.ID,\n\t\t\tUsername: mockRequest.Username,\n\t\t\tImage:    mockRequest.Image,\n\t\t\tType:     0,\n\t\t})\n\t}\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetRequests\", authUser.ID).Return(&requests, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account/me/pending\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(requests)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetRequests\", authUser.ID).Return(&requests, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account/me/pending\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetRequests\", authUser.ID, \"\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetRequests\", authUser.ID).Return(nil, fmt.Errorf(\"some error down call chain\"))\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/account/me/pending\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", authUser.ID)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_AcceptFriendRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tcurrent := fixture.GetMockUser()\n\n\tt.Run(\"Successfully accepted request\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tmockUser.Requests = append(mockUser.Requests, *current)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockFriendService.On(\"SaveRequests\", current).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tcurrent.Friends = append(current.Friends, *mockUser)\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\tmockFriendService.On(\"SaveRequests\", mockUser).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockUser.Friends = append(mockUser.Friends, *current)\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\tmockFriendService.On(\"DeleteRequest\", current.ID, mockUser.ID).Return(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitAddFriend\", current, mockUser).Return()\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/accept\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Member does not contain a request\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/accept\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"SaveRequests\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriendRequest\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"GetMemberById\", id).Return(nil, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/accept\", id)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"GetMemberById\", id)\n\t})\n\n\tt.Run(\"NotFound\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", mockUser.ID)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/accept\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"SaveRequests\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriendRequest\")\n\t})\n\n\tt.Run(\"MemberId and UserId are the same\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/accept\", current.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.AcceptYourselfError)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetMemberById\")\n\t\tmockFriendService.AssertNotCalled(t, \"SaveRequests\")\n\t\tmockFriendService.AssertNotCalled(t, \"DeleteRequest\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriend\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tmockUser.Requests = append(mockUser.Requests, *current)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.UnableAcceptError)\n\t\tmockFriendService.On(\"SaveRequests\", mockUser).\n\t\t\tReturn(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/accept\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriend\")\n\t})\n}\n\nfunc TestHandler_SendFriendRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tcurrent := fixture.GetMockUser()\n\n\tt.Run(\"Successfully send request\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockFriendService.On(\"SaveRequests\", current).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tcurrent.Requests = append(current.Requests, *mockUser)\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitAddFriendRequest\", mockUser.ID, &model.FriendRequest{\n\t\t\tId:       current.ID,\n\t\t\tUsername: current.Username,\n\t\t\tImage:    current.Image,\n\t\t\tType:     1,\n\t\t}).Return()\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Member already is a friend\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrent.Friends = append(current.Friends, *mockUser)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"SaveRequests\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriendRequest\")\n\t})\n\n\tt.Run(\"Member already contains request\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrent.Requests = append(current.Requests, *mockUser)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"SaveRequests\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriendRequest\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"GetMemberById\", id).Return(nil, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", id)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"GetMemberById\", id)\n\t})\n\n\tt.Run(\"NotFound\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", mockUser.ID)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"SaveRequests\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriendRequest\")\n\t})\n\n\tt.Run(\"MemberId and UserId are the same\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", current.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.AddYourselfError)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetMemberById\")\n\t\tmockFriendService.AssertNotCalled(t, \"SaveRequests\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriendRequest\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.UnableAddError)\n\t\tmockFriendService.On(\"SaveRequests\", current).\n\t\t\tReturn(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddFriendRequest\")\n\t})\n}\n\nfunc TestHandler_RemoveFriend(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tcurrent := fixture.GetMockUser()\n\n\tt.Run(\"Successfully removed friend\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrent.Friends = append(current.Friends, *mockUser)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockFriendService.On(\"RemoveFriend\", mockUser.ID, current.ID).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tcurrent.Friends = make([]model.User, 0)\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitRemoveFriend\", current.ID, mockUser.ID).Return()\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Member was not a friend\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"RemoveFriend\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFriend\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"GetMemberById\", id).Return(nil, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", id)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"GetMemberById\", id)\n\t})\n\n\tt.Run(\"NotFound\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", mockUser.ID)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"RemoveFriend\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFriend\")\n\t})\n\n\tt.Run(\"MemberId and UserId are the same\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", current.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.RemoveYourselfError)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetMemberById\")\n\t\tmockFriendService.AssertNotCalled(t, \"RemoveFriend\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFriend\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrent.Friends = append(current.Friends, *mockUser)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.UnableRemoveError)\n\t\tmockFriendService.On(\"RemoveFriend\", mockUser.ID, current.ID).\n\t\t\tReturn(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFriend\")\n\t})\n}\n\nfunc TestHandler_CancelFriendRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tcurrent := fixture.GetMockUser()\n\n\tt.Run(\"Successfully canceled request\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrent.Requests = append(current.Requests, *mockUser)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockFriendService.On(\"DeleteRequest\", mockUser.ID, current.ID).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tcurrent.Requests = make([]model.User, 0)\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/cancel\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Current does not contain a request\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/cancel\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"DeleteRequest\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"GetMemberById\", id).Return(nil, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:           router,\n\t\t\tUserService: mockUserService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/cancel\", id)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t\tmockUserService.AssertNotCalled(t, \"GetMemberById\", id)\n\t})\n\n\tt.Run(\"NotFound\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", mockUser.ID)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/cancel\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t\tmockFriendService.AssertNotCalled(t, \"DeleteRequest\")\n\t})\n\n\tt.Run(\"MemberId and UserId are the same\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/cancel\", current.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.CancelYourselfError)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetMemberById\")\n\t\tmockFriendService.AssertNotCalled(t, \"DeleteRequest\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrent.Requests = append(current.Requests, *mockUser)\n\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetMemberById\", current.ID).Return(current, nil)\n\t\tmockFriendService.On(\"GetMemberById\", mockUser.ID).Return(mockUser, nil)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.UnableRemoveError)\n\t\tmockFriendService.On(\"DeleteRequest\", mockUser.ID, current.ID).\n\t\t\tReturn(mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(current.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\turl := fmt.Sprintf(\"/api/account/%s/friend/cancel\", mockUser.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, url, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockFriendService.AssertExpectations(t)\n\t})\n}\n"
  },
  {
    "path": "server/handler/guild_handler.go",
    "content": "package handler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation/v4\"\n\t\"github.com/lib/pq\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n/*\n * GuildHandler contains all routes related to guild actions (/api/guilds)\n */\n\n// GetUserGuilds returns the current users guilds\n// GetUserGuilds godoc\n// @Tags Guilds\n// @Summary Get Current User's Guilds\n// @Produce  json\n// @Success 200 {array} model.GuildResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /guilds [get]\nfunc (h *Handler) GetUserGuilds(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tguilds, err := h.guildService.GetUserGuilds(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find guilds for id: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, guilds)\n}\n\n// GetGuildMembers returns the given guild's members\n// GetGuildMembers godoc\n// @Tags Guilds\n// @Summary Get Guild Members\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {array} model.MemberResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/members [get]\nfunc (h *Handler) GetGuildMembers(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tguildId := c.Param(\"guildId\")\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find guilds for id: %v\\n%v\", guildId, err)\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if a member\n\tif !isMember(guild, userId) {\n\t\te := apperrors.NewAuthorization(apperrors.NotAMember)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmembers, err := h.guildService.GetGuildMembers(userId, guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find guilds for id: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, members)\n}\n\n// GetVCMembers returns the given guild's members that are currently in the VC\n// GetVCMembers godoc\n// @Tags Guilds\n// @Summary Get Guild VC Members\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {array} model.VCMemberResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/vcmembers [get]\nfunc (h *Handler) GetVCMembers(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tguildId := c.Param(\"guildId\")\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find guilds for id: %v\\n%v\", guildId, err)\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if a member\n\tif !isMember(guild, userId) {\n\t\te := apperrors.NewAuthorization(apperrors.NotAMember)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmembers, err := h.guildService.GetVCMembers(guild.ID)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find vc members for id: %v\\n%v\", guildId, err)\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, members)\n}\n\ntype createGuildRequest struct {\n\t// Guild Name. 3 to 30 characters\n\tName string `json:\"name\"`\n} //@name CreateGuildRequest\n\nfunc (r createGuildRequest) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Name, validation.Required, validation.Length(3, 30)),\n\t)\n}\n\nfunc (r *createGuildRequest) sanitize() {\n\tr.Name = strings.TrimSpace(r.Name)\n}\n\n// CreateGuild creates a guild\n// CreateGuild godoc\n// @Tags Guilds\n// @Summary Create Guild\n// @Accepts  json\n// @Produce  json\n// @Param request body createGuildRequest true \"Create Guild\"\n// @Success 201 {array} model.GuildResponse\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/create [post]\nfunc (h *Handler) CreateGuild(c *gin.Context) {\n\tvar req createGuildRequest\n\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tauthUser, err := h.guildService.GetUser(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the user is already in 100 guilds\n\tif len(authUser.Guilds) >= model.MaximumGuilds {\n\t\te := apperrors.NewBadRequest(apperrors.GuildLimitReached)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tguildParams := model.Guild{\n\t\tName:    req.Name,\n\t\tOwnerId: userId,\n\t}\n\n\t// Add the current user as a member\n\tguildParams.Members = append(guildParams.Members, *authUser)\n\n\tguild, err := h.guildService.CreateGuild(&guildParams)\n\n\tif err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Create the default 'general' channel for the guild\n\tchannelParams := model.Channel{\n\t\tGuildID:  &guild.ID,\n\t\tName:     \"general\",\n\t\tIsPublic: true,\n\t}\n\n\tchannel, err := h.channelService.CreateChannel(&channelParams)\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to create channel for guild: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusCreated, guild.SerializeGuild(channel.ID))\n}\n\n// editGuildRequest specifies the form to edit the guild.\n// If Image is not nil then the guild's icon got changed.\n// If Icon is not nil then the guild kept its old one.\n// If both are nil then the icon got reset.\ntype editGuildRequest struct {\n\t// Guild Name. 3 to 30 characters\n\tName string `form:\"name\"`\n\t// image/png or image/jpeg\n\tImage *multipart.FileHeader `form:\"image\" swaggertype:\"string\" format:\"binary\"`\n\t// The old guild icon url if no new image is selected. Set to null to reset the guild icon\n\tIcon *string `form:\"icon\"`\n} //@name EditGuildRequest\n\nfunc (r editGuildRequest) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Name, validation.Required, validation.Length(3, 30)),\n\t)\n}\n\nfunc (r *editGuildRequest) sanitize() {\n\tr.Name = strings.TrimSpace(r.Name)\n}\n\n// EditGuild edits the given guild\n// EditGuild godoc\n// @Tags Guilds\n// @Summary Edit Guild\n// @Accepts  mpfd\n// @Produce  json\n// @Param request body editGuildRequest true \"Edit Guild\"\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId} [put]\nfunc (h *Handler) EditGuild(c *gin.Context) {\n\tvar req editGuildRequest\n\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tuserId := c.MustGet(\"userId\").(string)\n\tguildId := c.Param(\"guildId\")\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tguild.Name = req.Name\n\n\t// Guild icon got changed\n\tif req.Image != nil {\n\t\t// Validate image mime-type is allowable\n\t\tmimeType := req.Image.Header.Get(\"Content-Type\")\n\n\t\tif valid := isAllowedImageType(mimeType); !valid {\n\t\t\ttoFieldErrorResponse(c, \"Image\", apperrors.InvalidImageType)\n\t\t\treturn\n\t\t}\n\n\t\tdirectory := fmt.Sprintf(\"valkyrie/guilds/%s\", guild.ID)\n\t\turl, err := h.userService.ChangeAvatar(req.Image, directory)\n\n\t\tif err != nil {\n\t\t\te := apperrors.NewInternal()\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tif guild.Icon != nil {\n\t\t\t_ = h.userService.DeleteImage(*guild.Icon)\n\t\t}\n\t\tguild.Icon = &url\n\t\t// Guild kept its old icon\n\t} else if req.Icon != nil {\n\t\tguild.Icon = req.Icon\n\t\t// Guild reset its icon\n\t} else {\n\t\tguild.Icon = nil\n\t}\n\n\tif err = h.guildService.UpdateGuild(guild); err != nil {\n\t\tlog.Printf(\"Failed to update guild: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit guild changes to guild members\n\th.socketService.EmitEditGuild(guild)\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// GetInvite creates an invitation for the given guild\n// The isPermanent query parameter specifies if the invite\n// should not be deleted after it got used\n// GetInvite godoc\n// @Tags Guilds\n// @Summary Get Guild Invite\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Param isPermanent query boolean false \"Is Permanent\"\n// @Success 200 string link\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/invite [get]\nfunc (h *Handler) GetInvite(c *gin.Context) {\n\tguildId := c.Param(\"guildId\")\n\tpermanent := c.Query(\"isPermanent\")\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tuserId := c.MustGet(\"userId\").(string)\n\t// Must be a member to create an invitation\n\tif !isMember(guild, userId) {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeMemberInvite)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tisPermanent := false\n\tif permanent != \"\" {\n\t\tisPermanent, err = strconv.ParseBool(permanent)\n\n\t\tif err != nil {\n\t\t\te := apperrors.NewBadRequest(apperrors.IsPermanentError)\n\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t}\n\n\tctx := context.Background()\n\tlink, err := h.guildService.GenerateInviteLink(ctx, guild.ID, isPermanent)\n\n\tif err != nil {\n\t\te := apperrors.NewInternal()\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif isPermanent {\n\t\tguild.InviteLinks = append(guild.InviteLinks, link)\n\t\t_ = h.guildService.UpdateGuild(guild)\n\t}\n\n\torigin := os.Getenv(\"CORS_ORIGIN\")\n\tc.JSON(http.StatusOK, fmt.Sprintf(\"%s/%s\", origin, link))\n}\n\n// DeleteGuildInvites removes all permanent invites from the given guild\n// DeleteGuildInvites godoc\n// @Tags Guilds\n// @Summary Delete all permanent invite links\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {object} model.Success\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/invite [delete]\nfunc (h *Handler) DeleteGuildInvites(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tguildId := c.Param(\"guildId\")\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.InvalidateInvitesError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tctx := context.Background()\n\th.guildService.InvalidateInvites(ctx, guild)\n\tguild.InviteLinks = make(pq.StringArray, 0)\n\n\tif err = h.guildService.UpdateGuild(guild); err != nil {\n\t\tlog.Printf(\"Failed to delete guild invites: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\ntype joinReq struct {\n\tLink string `json:\"link\"`\n} //@name JoinRequest\n\nfunc (r joinReq) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Link, validation.Required),\n\t)\n}\n\nfunc (r *joinReq) sanitize() {\n\tr.Link = strings.TrimSpace(r.Link)\n}\n\n// JoinGuild adds the current user to invited guild\n// JoinGuild godoc\n// @Tags Guilds\n// @Summary Join Guild\n// @Produce  json\n// @Param request body joinReq true \"Join Guild\"\n// @Success 200 {object} model.GuildResponse\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/join [post]\nfunc (h *Handler) JoinGuild(c *gin.Context) {\n\tvar req joinReq\n\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tauthUser, err := h.guildService.GetUser(userId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the user has reached the guild limit\n\tif len(authUser.Guilds) >= model.MaximumGuilds {\n\t\te := apperrors.NewBadRequest(apperrors.GuildLimitReached)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// If the link contains the domain, remove it\n\tif strings.Contains(req.Link, \"/\") {\n\t\treq.Link = req.Link[strings.LastIndex(req.Link, \"/\")+1:]\n\t}\n\n\tctx := context.Background()\n\tguildId, err := h.guildService.GetGuildIdFromInvite(ctx, req.Link)\n\n\tif err != nil {\n\t\te := apperrors.NewBadRequest(apperrors.InvalidInviteError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewBadRequest(apperrors.InvalidInviteError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the user is banned from the guild\n\tif isBanned(guild, authUser.ID) {\n\t\te := apperrors.NewBadRequest(apperrors.BannedFromServer)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the user is already a member\n\tif isMember(guild, authUser.ID) {\n\t\te := apperrors.NewBadRequest(apperrors.AlreadyMember)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tguild.Members = append(guild.Members, *authUser)\n\n\tif err = h.guildService.UpdateGuild(guild); err != nil {\n\t\tlog.Printf(\"Failed to join guild: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit new member to the guild\n\th.socketService.EmitAddMember(guild.ID, authUser)\n\n\tchannel, _ := h.guildService.GetDefaultChannel(guildId)\n\n\tc.JSON(http.StatusCreated, guild.SerializeGuild(channel.ID))\n}\n\n// LeaveGuild leaves the given guild\n// LeaveGuild godoc\n// @Tags Guilds\n// @Summary Leave Guild\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {object} model.Success\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId} [delete]\nfunc (h *Handler) LeaveGuild(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tguildId := c.Param(\"guildId\")\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif guild.OwnerId == userId {\n\t\te := apperrors.NewAuthorization(apperrors.OwnerCantLeave)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif err := h.guildService.RemoveMember(userId, guildId); err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit signal to remove the member from the guild\n\th.socketService.EmitRemoveMember(guild.ID, userId)\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// DeleteGuild deletes the given guild\n// DeleteGuild godoc\n// @Tags Guilds\n// @Summary Delete Guild\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {object} model.Success\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/delete [delete]\nfunc (h *Handler) DeleteGuild(c *gin.Context) {\n\tuserId := c.MustGet(\"userId\").(string)\n\tguildId := c.Param(\"guildId\")\n\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.DeleteGuildError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Get the ID of all members to emit the deletion to\n\tmembers := make([]string, 0)\n\tfor _, member := range guild.Members {\n\t\tmembers = append(members, member.ID)\n\t}\n\n\tif err := h.guildService.DeleteGuild(guildId); err != nil {\n\t\tlog.Printf(\"Failed to leave guild: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit signal to remove the guild to its members\n\th.socketService.EmitDeleteGuild(guildId, members)\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// isMember checks if the given user is a member of the guild\nfunc isMember(guild *model.Guild, userId string) bool {\n\tfor _, v := range guild.Members {\n\t\tif v.ID == userId {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// isBanned checks if the given user is banned from the guild\nfunc isBanned(guild *model.Guild, userId string) bool {\n\tfor _, v := range guild.Bans {\n\t\tif v.ID == userId {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/handler/guild_handler_test.go",
    "content": "package handler\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestHandler_GetUserGuilds(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tresponse := make([]model.GuildResponse, 0)\n\tfor i := 0; i < 5; i++ {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tresponse = append(response, mockGuild.SerializeGuild(fixture.RandID()))\n\t}\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUserGuilds\", authUser.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(response)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockFriendService := new(mocks.FriendService)\n\t\tmockFriendService.On(\"GetUserGuilds\", authUser.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tFriendService: mockFriendService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\t\tmockFriendService.AssertNotCalled(t, \"GetUserGuilds\", authUser.ID, \"\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUserGuilds\", authUser.ID).Return(nil, fmt.Errorf(\"some error down call chain\"))\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/guilds\", nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", authUser.ID)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_GetGuildMembers(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\tguild := fixture.GetMockGuild(\"\")\n\tguild.Members = append(guild.Members, *authUser)\n\n\tresponse := make([]model.MemberResponse, 0)\n\tfor i := 0; i < 5; i++ {\n\t\tmockUser := fixture.GetMockUser()\n\t\tresponse = append(response, model.MemberResponse{\n\t\t\tId:        mockUser.ID,\n\t\t\tUsername:  mockUser.Username,\n\t\t\tImage:     mockUser.Image,\n\t\t\tIsOnline:  mockUser.IsOnline,\n\t\t\tCreatedAt: mockUser.CreatedAt,\n\t\t\tUpdatedAt: mockUser.UpdatedAt,\n\t\t\tIsFriend:  false,\n\t\t})\n\t}\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", guild.ID).Return(guild, nil)\n\t\tmockGuildService.On(\"GetGuildMembers\", authUser.ID, guild.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/members\", guild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(response)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", guild.ID).Return(guild, nil)\n\t\tmockGuildService.On(\"GetGuildMembers\", authUser.ID, guild.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/members\", guild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\", guild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuildMembers\", authUser.ID, guild.ID)\n\t})\n\n\tt.Run(\"Not a member of the guild\", func(t *testing.T) {\n\n\t\tinvalidGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", invalidGuild.ID).Return(invalidGuild, nil)\n\t\tmockGuildService.On(\"GetGuildMembers\", authUser.ID, invalidGuild.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/members\", invalidGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.NotAMember)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", invalidGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuildMembers\", authUser.ID, invalidGuild.ID)\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\n\t\tinvalidGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", invalidGuild.ID)\n\t\tmockGuildService.On(\"GetGuild\", invalidGuild.ID).Return(nil, mockError)\n\t\tmockGuildService.On(\"GetGuildMembers\", authUser.ID, invalidGuild.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/members\", invalidGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", invalidGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuildMembers\", authUser.ID, invalidGuild.ID)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", guild.ID).Return(guild, nil)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", authUser.ID)\n\t\tmockGuildService.On(\"GetGuildMembers\", authUser.ID, guild.ID).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/members\", guild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", guild.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetGuildMembers\", authUser.ID, guild.ID)\n\t})\n}\n\nfunc TestHandler_GetVCMembers(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\tguild := fixture.GetMockGuild(\"\")\n\tguild.Members = append(guild.Members, *authUser)\n\n\tresponse := make([]model.VCMemberResponse, 0)\n\n\tfor i := 0; i < 5; i++ {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tguild.VCMembers = append(guild.VCMembers, *authUser)\n\t\tresponse = append(response, model.VCMemberResponse{\n\t\t\tId:         mockUser.ID,\n\t\t\tUsername:   mockUser.Username,\n\t\t\tImage:      mockUser.Image,\n\t\t\tIsMuted:    false,\n\t\t\tIsDeafened: false,\n\t\t})\n\t}\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", guild.ID).Return(guild, nil)\n\t\tmockGuildService.On(\"GetVCMembers\", guild.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/vcmembers\", guild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(response)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", guild.ID).Return(guild, nil)\n\t\tmockGuildService.On(\"GetVCMembers\", guild.ID).Return(response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/vcmembers\", guild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\", guild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetVCMembers\", guild.ID)\n\t})\n\n\tt.Run(\"Not a member of the guild\", func(t *testing.T) {\n\t\tinvalidGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", invalidGuild.ID).Return(invalidGuild, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/vcmembers\", invalidGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.NotAMember)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", invalidGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetVCMembers\", guild.ID)\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\n\t\tinvalidGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", invalidGuild.ID)\n\t\tmockGuildService.On(\"GetGuild\", invalidGuild.ID).Return(nil, mockError)\n\t\tmockGuildService.On(\"GetVCMembers\", guild.ID).Return(&response, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/vcmembers\", invalidGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", invalidGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetVCMembers\", guild.ID)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockGuildService.On(\"GetGuild\", guild.ID).Return(guild, nil)\n\t\tmockError := apperrors.NewNotFound(\"guild\", guild.ID)\n\t\tmockGuildService.On(\"GetVCMembers\", guild.ID).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/vcmembers\", guild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", guild.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetVCMembers\", guild.ID)\n\t})\n}\n\nfunc TestHandler_CreateGuild(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful guild creation\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockGuild.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tguildParams := &model.Guild{\n\t\t\tName:    mockGuild.Name,\n\t\t\tOwnerId: authUser.ID,\n\t\t}\n\t\tguildParams.Members = append(guildParams.Members, *authUser)\n\n\t\tmockGuildService.On(\"CreateGuild\", guildParams).Return(mockGuild, nil)\n\n\t\tdefaultChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tdefaultChannel.Name = \"general\"\n\t\tmockGuild.Channels = append(mockGuild.Channels, *defaultChannel)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tchannelParams := &model.Channel{\n\t\t\tGuildID:  &mockGuild.ID,\n\t\t\tName:     defaultChannel.Name,\n\t\t\tIsPublic: true,\n\t\t}\n\n\t\tmockChannelService.On(\"CreateChannel\", channelParams).Return(defaultChannel, nil)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(mockGuild.SerializeGuild(defaultChannel.ID))\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusCreated, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetUser\", authUser.ID)\n\t\tmockGuildService.AssertCalled(t, \"CreateGuild\", guildParams)\n\t\tmockChannelService.AssertCalled(t, \"CreateChannel\", channelParams)\n\t})\n\n\tt.Run(\"Error Returned from GuildService.CreateGuild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockGuild.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tguildParams := &model.Guild{\n\t\t\tName:    mockGuild.Name,\n\t\t\tOwnerId: authUser.ID,\n\t\t}\n\t\tguildParams.Members = append(guildParams.Members, *authUser)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockGuildService.On(\"CreateGuild\", guildParams).Return(nil, mockError)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetUser\", authUser.ID)\n\t\tmockGuildService.AssertCalled(t, \"CreateGuild\", guildParams)\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t})\n\n\tt.Run(\"User already is in the maximum number of guilds\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tfor i := 0; i < model.MaximumGuilds; i++ {\n\t\t\tguild := fixture.GetMockGuild(\"\")\n\t\t\tauthUser.Guilds = append(authUser.Guilds, *guild)\n\t\t}\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": mockGuild.Name,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.GuildLimitReached)\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetUser\", authUser.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"CreateGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\trouter := getTestRouter()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockChannelService := new(mocks.ChannelService)\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"name\": fixture.RandStringRunes(6),\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"CreateGuild\")\n\t\tmockChannelService.AssertNotCalled(t, \"CreateChannel\")\n\t})\n}\n\nfunc TestHandler_CreateGuild_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\trouter := getAuthenticatedTestRouter(mockUser.ID)\n\n\tmockGuildService := new(mocks.GuildService)\n\tmockGuildService.On(\"GetUser\", mockUser.ID).Return(mockUser, nil)\n\n\tNewHandler(&Config{\n\t\tR:            router,\n\t\tGuildService: mockGuildService,\n\t})\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody gin.H\n\t}{\n\t\t{\n\t\t\tname: \"Name required\",\n\t\t\tbody: gin.H{},\n\t\t},\n\t\t{\n\t\t\tname: \"Name too short\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"name\": fixture.RandStr(2),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Name too long\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"name\": fixture.RandStr(31),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\treqBody, err := json.Marshal(tc.body)\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/create\", bytes.NewBuffer(reqBody))\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockGuildService.AssertNotCalled(t, \"CreateGuild\")\n\t\t})\n\t}\n}\n\nfunc TestHandler_UpdateGuild(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully updated guild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tname := fixture.RandStringRunes(8)\n\t\tform := url.Values{}\n\t\tform.Add(\"name\", name)\n\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockGuild.Name = name\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitEditGuild\", mockGuild).Return()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/guilds/\"+mockGuild.ID, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertCalled(t, \"UpdateGuild\", mockGuild)\n\t\tmockSocketService.AssertCalled(t, \"EmitEditGuild\", mockGuild)\n\t})\n\n\tt.Run(\"Error Returned from GuildService.UpdateGuild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tname := fixture.RandStringRunes(8)\n\t\tform := url.Values{}\n\t\tform.Add(\"name\", name)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitEditGuild\", mockGuild).Return()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/guilds/\"+mockGuild.ID, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertCalled(t, \"UpdateGuild\", mockGuild)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditGuild\", mockGuild)\n\t})\n\n\tt.Run(\"Not the owner of the guild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tname := fixture.RandStringRunes(8)\n\t\tform := url.Values{}\n\t\tform.Add(\"name\", name)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/guilds/\"+mockGuild.ID, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\", mockGuild)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditGuild\", mockGuild)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\trouter := getTestRouter()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tname := fixture.RandStringRunes(8)\n\t\tform := url.Values{}\n\t\tform.Add(\"name\", name)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/guilds/\"+mockGuild.ID, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\", mockGuild)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditGuild\", mockGuild)\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", id)\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", id).Return(nil, mockError)\n\n\t\tname := fixture.RandStringRunes(8)\n\t\tform := url.Values{}\n\t\tform.Add(\"name\", name)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/guilds/\"+id, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\", mock.AnythingOfType(\"*model.Guild\"))\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditGuild\", mock.AnythingOfType(\"*model.Guild\"))\n\t})\n}\n\nfunc TestHandler_UpdateGuild_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\trouter := getAuthenticatedTestRouter(mockUser.ID)\n\n\tmockGuildService := new(mocks.GuildService)\n\tmockGuildService.On(\"GetUser\", mockUser.ID).Return(mockUser, nil)\n\n\tNewHandler(&Config{\n\t\tR:            router,\n\t\tGuildService: mockGuildService,\n\t})\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody url.Values\n\t}{\n\t\t{\n\t\t\tname: \"Name required\",\n\t\t\tbody: map[string][]string{},\n\t\t},\n\t\t{\n\t\t\tname: \"Name too short\",\n\t\t\tbody: map[string][]string{\n\t\t\t\t\"name\": {fixture.RandStr(2)},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Name too long\",\n\t\t\tbody: map[string][]string{\n\t\t\t\t\"name\": {fixture.RandStr(31)},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s\", fixture.RandID())\n\t\t\tform := tc.body\n\t\t\trequest, _ := http.NewRequest(http.MethodPut, reqUrl, strings.NewReader(form.Encode()))\n\t\t\trequest.Form = form\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetInvite(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\torigin := \"http://localhost:3000\"\n\n\terr := os.Setenv(\"CORS_ORIGIN\", origin)\n\tassert.NoError(t, err)\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tgetInviteArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockGuild.ID,\n\t\t\tfalse,\n\t\t}\n\n\t\tlink := fixture.RandID()\n\n\t\tmockGuildService.On(\"GenerateInviteLink\", getInviteArgs...).Return(link, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(fmt.Sprintf(\"%s/%s\", origin, link))\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t})\n\n\tt.Run(\"Not a member of the guild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.MustBeMemberInvite)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GenerateInviteLink\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", id)\n\t\tmockGuildService.On(\"GetGuild\", id).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", id)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"GenerateInviteLink\")\n\t})\n\n\tt.Run(\"Invalid isPermanent value\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite?isPermanent=yes\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.IsPermanentError)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GenerateInviteLink\")\n\t})\n\n\tt.Run(\"Invite isPermanent success\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\n\t\tlink := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockGuild.InviteLinks = append(mockGuild.InviteLinks, link)\n\t\t\t}).\n\t\t\tReturn(nil)\n\n\t\tgetInviteArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockGuild.ID,\n\t\t\ttrue,\n\t\t}\n\n\t\tmockGuildService.On(\"GenerateInviteLink\", getInviteArgs...).Return(link, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite?isPermanent=true\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(fmt.Sprintf(\"%s/%s\", origin, link))\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tgetInviteArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockGuild.ID,\n\t\t\tfalse,\n\t\t}\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockGuildService.On(\"GenerateInviteLink\", getInviteArgs...).Return(\"\", mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_DeleteGuildInvites(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully deleted\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockGuild,\n\t\t}\n\n\t\tmockGuildService.On(\"InvalidateInvites\", mockArgs...).Return()\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tmockGuild,\n\t\t}\n\n\t\tmockGuildService.On(\"InvalidateInvites\", mockArgs...).Return()\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Not the server owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidateInvitesError)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"InvalidateInvites\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockError := apperrors.NewNotFound(\"guild\", id)\n\t\tmockGuildService.On(\"GetGuild\", id).Return(nil, mockError)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/invite\", id)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"InvalidateInvites\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t})\n}\n\nfunc TestHandler_JoinGuild(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully joined\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tlink := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tlink,\n\t\t}\n\n\t\tmockGuildService.On(\"GetGuildIdFromInvite\", mockArgs...).Return(mockGuild.ID, nil)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(nil)\n\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockGuildService.On(\"GetDefaultChannel\", mockGuild.ID).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitAddMember\", mockGuild.ID, authUser).Return()\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"link\": link,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(mockGuild.SerializeGuild(mockChannel.ID))\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusCreated, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Link required\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"link\": \"\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuildIdFromInvite\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetDefaultChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddMember\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"link\": \"link\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuildIdFromInvite\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetDefaultChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddMember\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tlink := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tlink,\n\t\t}\n\n\t\tmockGuildService.On(\"GetGuildIdFromInvite\", mockArgs...).Return(mockGuild.ID, nil)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"link\": link,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockGuildService.AssertNotCalled(t, \"GetDefaultChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddMember\")\n\t})\n\n\tt.Run(\"User is already in the maximum number of guilds\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\tnewAuthUser := fixture.GetMockUser()\n\t\tfor i := 0; i < model.MaximumGuilds; i++ {\n\t\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\t\tnewAuthUser.Guilds = append(newAuthUser.Guilds, *mockGuild)\n\t\t}\n\n\t\tmockGuildService.On(\"GetUser\", newAuthUser.ID).Return(newAuthUser, nil)\n\n\t\trouter := getAuthenticatedTestRouter(newAuthUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"link\": \"link\",\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.GuildLimitReached)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetUser\", newAuthUser.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuildIdFromInvite\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetDefaultChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddMember\")\n\t})\n\n\tt.Run(\"User is banned from the guild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Bans = append(mockGuild.Bans, *authUser)\n\t\tlink := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tlink,\n\t\t}\n\n\t\tmockGuildService.On(\"GetGuildIdFromInvite\", mockArgs...).Return(mockGuild.ID, nil)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"link\": link,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.BannedFromServer)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockGuildService.AssertNotCalled(t, \"GetDefaultChannel\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddMember\")\n\t})\n\n\tt.Run(\"Invalid Invite\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tlink := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tlink,\n\t\t}\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.InvalidInviteError)\n\t\tmockGuildService.On(\"GetGuildIdFromInvite\", mockArgs...).Return(mockGuild.ID, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"link\": link,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetDefaultChannel\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddMember\")\n\t})\n\n\tt.Run(\"Already a member\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\t\tlink := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmock.AnythingOfType(\"*context.emptyCtx\"),\n\t\t\tlink,\n\t\t}\n\n\t\tmockGuildService.On(\"GetGuildIdFromInvite\", mockArgs...).Return(mockGuild.ID, nil)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"link\": link,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/guilds/join\", bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.AlreadyMember)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockGuildService.AssertNotCalled(t, \"GetDefaultChannel\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitAddMember\")\n\t})\n}\n\nfunc TestHandler_LeaveGuild(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully left the guild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"RemoveMember\", authUser.ID, mockGuild.ID).Return(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitRemoveMember\", mockGuild.ID, authUser.ID).Return()\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"AuthUser is the owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.OwnerCantLeave)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", id)\n\t\tmockGuildService.On(\"GetGuild\", id).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s\", id)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockGuildService.On(\"RemoveMember\", authUser.ID, mockGuild.ID).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t})\n}\n\nfunc TestHandler_DeleteGuild(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully deleted\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"DeleteGuild\", mockGuild.ID).Return(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\tmembers := make([]string, 0)\n\t\tmockSocketService.On(\"EmitDeleteGuild\", mockGuild.ID, members).Return()\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/delete\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Not the guild owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/delete\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.DeleteGuildError)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"DeleteGuild\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteGuild\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/delete\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"DeleteGuild\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteGuild\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", id)\n\t\tmockGuildService.On(\"GetGuild\", id).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/delete\", id)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"DeleteGuild\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteGuild\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockGuildService.On(\"DeleteGuild\", mockGuild.ID).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/delete\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteGuild\")\n\t})\n}\n"
  },
  {
    "path": "server/handler/handler.go",
    "content": "package handler\n\nimport (\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-gonic/contrib/static\"\n\t\"github.com/gin-gonic/gin\"\n\t\"log\"\n\t\"net/http\"\n\n\t// Register swagger docs\n\t_ \"github.com/sentrionic/valkyrie/docs\"\n\t\"github.com/sentrionic/valkyrie/handler/middleware\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/swaggo/files\"       // swagger embed files\n\t\"github.com/swaggo/gin-swagger\" // gin-swagger middleware\n\t\"time\"\n)\n\n// Handler struct holds required services for handler to function\ntype Handler struct {\n\tuserService    model.UserService\n\tfriendService  model.FriendService\n\tguildService   model.GuildService\n\tchannelService model.ChannelService\n\tmessageService model.MessageService\n\tsocketService  model.SocketService\n\tMaxBodyBytes   int64\n}\n\n// Config will hold services that will eventually be injected into this\n// handler layer on handler initialization\ntype Config struct {\n\tR               *gin.Engine\n\tUserService     model.UserService\n\tFriendService   model.FriendService\n\tGuildService    model.GuildService\n\tChannelService  model.ChannelService\n\tMessageService  model.MessageService\n\tSocketService   model.SocketService\n\tTimeoutDuration time.Duration\n\tMaxBodyBytes    int64\n}\n\n// NewHandler initializes the handler with required injected services along with http routes\n// Does not return as it deals directly with a reference to the gin Engine\nfunc NewHandler(c *Config) {\n\n\t// Create a handler (which will later have injected services)\n\th := &Handler{\n\t\tuserService:    c.UserService,\n\t\tfriendService:  c.FriendService,\n\t\tguildService:   c.GuildService,\n\t\tchannelService: c.ChannelService,\n\t\tmessageService: c.MessageService,\n\t\tsocketService:  c.SocketService,\n\t\tMaxBodyBytes:   c.MaxBodyBytes,\n\t}\n\n\tc.R.NoRoute(func(c *gin.Context) {\n\t\tc.JSON(http.StatusNotFound, gin.H{\n\t\t\t\"error\": \"No route found. Go to https://api.valkyrieapp.xyz/swagger/index.html for a list of all routes\",\n\t\t})\n\t})\n\n\tc.R.Use(static.Serve(\"/\", static.LocalFile(\"./static\", true)))\n\n\tif gin.Mode() != gin.TestMode {\n\t\tc.R.Use(middleware.Timeout(c.TimeoutDuration, apperrors.NewServiceUnavailable()))\n\t}\n\n\tc.R.GET(\"/swagger/*any\", ginSwagger.WrapHandler(swaggerFiles.Handler))\n\n\t// Create an account group\n\tag := c.R.Group(\"api/account\")\n\n\tag.POST(\"/register\", h.Register)\n\tag.POST(\"/login\", h.Login)\n\tag.POST(\"/logout\", h.Logout)\n\tag.POST(\"/forgot-password\", h.ForgotPassword)\n\tag.POST(\"/reset-password\", h.ResetPassword)\n\n\tag.Use(middleware.AuthUser())\n\tag.GET(\"\", h.GetCurrent)\n\tag.PUT(\"\", h.Edit)\n\tag.PUT(\"/change-password\", h.ChangePassword)\n\n\tag.GET(\"/me/friends\", h.GetUserFriends)\n\tag.GET(\"/me/pending\", h.GetUserRequests)\n\tag.POST(\"/:memberId/friend\", h.SendFriendRequest)\n\tag.DELETE(\"/:memberId/friend\", h.RemoveFriend)\n\tag.POST(\"/:memberId/friend/accept\", h.AcceptFriendRequest)\n\tag.POST(\"/:memberId/friend/cancel\", h.CancelFriendRequest)\n\n\t// Create a guild group\n\tgg := c.R.Group(\"api/guilds\")\n\tgg.Use(middleware.AuthUser())\n\n\tgg.GET(\"/:guildId/members\", h.GetGuildMembers)\n\tgg.GET(\"/:guildId/vcmembers\", h.GetVCMembers)\n\tgg.GET(\"\", h.GetUserGuilds)\n\tgg.POST(\"/create\", h.CreateGuild)\n\tgg.GET(\"/:guildId/invite\", h.GetInvite)\n\tgg.DELETE(\"/:guildId/invite\", h.DeleteGuildInvites)\n\tgg.POST(\"/join\", h.JoinGuild)\n\tgg.GET(\"/:guildId/member\", h.GetMemberSettings)\n\tgg.PUT(\"/:guildId/member\", h.EditMemberSettings)\n\tgg.DELETE(\"/:guildId\", h.LeaveGuild)\n\tgg.PUT(\"/:guildId\", h.EditGuild)\n\tgg.DELETE(\"/:guildId/delete\", h.DeleteGuild)\n\tgg.GET(\"/:guildId/bans\", h.GetBanList)\n\tgg.POST(\"/:guildId/bans\", h.BanMember)\n\tgg.DELETE(\"/:guildId/bans\", h.UnbanMember)\n\tgg.POST(\"/:guildId/kick\", h.KickMember)\n\n\t// Create a channels group\n\tcg := c.R.Group(\"api/channels\")\n\tcg.Use(middleware.AuthUser())\n\n\t// Route parameters cause conflicts so they have to use the same parameter name\n\tcg.GET(\"/:id\", h.GuildChannels)                 // id -> guildId\n\tcg.POST(\"/:id\", h.CreateChannel)                // id -> guildId\n\tcg.GET(\"/:id/members\", h.PrivateChannelMembers) // id -> channelId\n\tcg.POST(\"/:id/dm\", h.GetOrCreateDM)             // id -> memberId\n\tcg.GET(\"/me/dm\", h.DirectMessages)              //\n\tcg.PUT(\"/:id\", h.EditChannel)                   // id -> channelId\n\tcg.DELETE(\"/:id\", h.DeleteChannel)              // id -> channelId\n\tcg.DELETE(\"/:id/dm\", h.CloseDM)                 // id -> channelId\n\n\t// Create a messages group\n\tmg := c.R.Group(\"api/messages\")\n\tmg.Use(middleware.AuthUser())\n\n\tmg.GET(\"/:channelId\", h.GetMessages)\n\tmg.POST(\"/:channelId\", h.CreateMessage)\n\tmg.PUT(\"/:messageId\", h.EditMessage)\n\tmg.DELETE(\"/:messageId\", h.DeleteMessage)\n}\n\n// setUserSession saves the users ID in the session\nfunc setUserSession(c *gin.Context, id string) {\n\tsession := sessions.Default(c)\n\tsession.Set(\"userId\", id)\n\tif err := session.Save(); err != nil {\n\t\tlog.Printf(\"error setting the session: %v\\n\", err.Error())\n\t}\n}\n\nfunc toFieldErrorResponse(c *gin.Context, field, message string) {\n\tc.JSON(http.StatusBadRequest, gin.H{\n\t\t\"errors\": []model.FieldError{\n\t\t\t{\n\t\t\t\tField:   field,\n\t\t\t\tMessage: message,\n\t\t\t},\n\t\t},\n\t})\n}\n"
  },
  {
    "path": "server/handler/member_handler.go",
    "content": "package handler\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation/v4\"\n\t\"github.com/go-ozzo/ozzo-validation/v4/is\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n/*\n * MemberHandler contains all routes related to mod actions (/api/guilds)\n */\n\n// memberReq contains the MemberId of the user\n// that needs to be moderated\ntype memberReq struct {\n\tMemberId string `json:\"memberId\"`\n} //@name MemberRequest\n\nfunc (r memberReq) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.MemberId, validation.Required, is.UTFDigit),\n\t)\n}\n\n// GetMemberSettings gets the current user's role color and nickname\n// for the given guild\n// GetMemberSettings godoc\n// @Tags Members\n// @Summary Get Member Settings\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {object} model.MemberSettings\n// @Failure 404 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/member [get]\nfunc (h *Handler) GetMemberSettings(c *gin.Context) {\n\tguildId := c.Param(\"guildId\")\n\tuserId := c.MustGet(\"userId\").(string)\n\t_, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tsettings, err := h.guildService.GetMemberSettings(userId, guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find settings: %v\\n%v\", userId, err)\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, settings)\n}\n\ntype memberSettingsReq struct {\n\tNickname *string `json:\"nickname\"`\n\tColor    *string `json:\"color\"`\n} //@name MemberSettingsRequest\n\nfunc (r memberSettingsReq) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Nickname, validation.NilOrNotEmpty, validation.Length(3, 30)),\n\t\tvalidation.Field(&r.Color, validation.NilOrNotEmpty, is.HexColor),\n\t)\n}\n\nfunc (r *memberSettingsReq) sanitize() {\n\tif r.Nickname != nil {\n\t\tnickname := strings.TrimSpace(*r.Nickname)\n\t\tr.Nickname = &nickname\n\t}\n}\n\n// EditMemberSettings changes the current user's role color and nickname\n// for the given guild\n// EditMemberSettings godoc\n// @Tags Members\n// @Summary Edit Member Settings\n// @Accepts json\n// @Produce  json\n// @Param request body memberSettingsReq true \"Edit Member\"\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/member [put]\nfunc (h *Handler) EditMemberSettings(c *gin.Context) {\n\tvar req memberSettingsReq\n\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tguildId := c.Param(\"guildId\")\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tuserId := c.MustGet(\"userId\").(string)\n\n\t// Check if the user is a member of the guild\n\tif !isMember(guild, userId) {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tsettings := &model.MemberSettings{\n\t\tNickname: req.Nickname,\n\t\tColor:    req.Color,\n\t}\n\n\terr = h.guildService.UpdateMemberSettings(settings, userId, guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to update settings for user: %v\\n%v\", userId, err)\n\t\te := apperrors.NewInternal()\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// GetBanList returns a list of all banned users for the given guild\n// GetBanList godoc\n// @Tags Members\n// @Summary Get Guild Ban list\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Success 200 {array} model.BanResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/bans [get]\nfunc (h *Handler) GetBanList(c *gin.Context) {\n\tguildId := c.Param(\"guildId\")\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tbans, err := h.guildService.GetBanList(guildId)\n\n\tif err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// If the guild does not have any bans, return an empty array\n\tif len(*bans) == 0 {\n\t\tempty := make([]model.BanResponse, 0)\n\t\tc.JSON(http.StatusOK, empty)\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, bans)\n}\n\n// BanMember bans the provided member from the given guild\n// BanMember godoc\n// @Tags Members\n// @Summary Ban Member\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Param request body memberReq true \"Member ID\"\n// @Success 200 {array} model.Success\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/bans [post]\nfunc (h *Handler) BanMember(c *gin.Context) {\n\tvar req memberReq\n\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\tguildId := c.Param(\"guildId\")\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmember, err := h.guildService.GetUser(req.MemberId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", req.MemberId, err)\n\t\te := apperrors.NewNotFound(\"user\", req.MemberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif member.ID == userId {\n\t\te := apperrors.NewBadRequest(apperrors.BanYourselfError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tguild.Bans = append(guild.Bans, *member)\n\n\tif err = h.guildService.UpdateGuild(guild); err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\terr = h.guildService.RemoveMember(req.MemberId, guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to ban member: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit signals to remove the member from the guild\n\th.socketService.EmitRemoveMember(guild.ID, member.ID)\n\th.socketService.EmitRemoveFromGuild(member.ID, guildId)\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// UnbanMember unbans the specified user from the given guild\n// BanMember godoc\n// @Tags Members\n// @Summary Unban Member\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Param request body memberReq true \"Member ID\"\n// @Success 200 {array} model.Success\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/bans [delete]\nfunc (h *Handler) UnbanMember(c *gin.Context) {\n\tvar req memberReq\n\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\tguildId := c.Param(\"guildId\")\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif req.MemberId == userId {\n\t\te := apperrors.NewBadRequest(apperrors.UnbanYourselfError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif err := h.guildService.UnbanMember(req.MemberId, guildId); err != nil {\n\t\tlog.Printf(\"Failed to unban member: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// KickMember kicks the provided member from the given guild\n// KickMember godoc\n// @Tags Members\n// @Summary Kick Member\n// @Produce  json\n// @Param guildId path string true \"Guild ID\"\n// @Param request body memberReq true \"Member ID\"\n// @Success 200 {array} model.Success\n// @Failure 400 {object} model.ErrorResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /guilds/{guildId}/kick [post]\nfunc (h *Handler) KickMember(c *gin.Context) {\n\tvar req memberReq\n\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\tguildId := c.Param(\"guildId\")\n\tguild, err := h.guildService.GetGuild(guildId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"guild\", guildId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tif guild.OwnerId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmember, err := h.guildService.GetUser(req.MemberId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to find user: %v\\n%v\", req.MemberId, err)\n\t\te := apperrors.NewNotFound(\"user\", req.MemberId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif member.ID == userId {\n\t\te := apperrors.NewBadRequest(apperrors.KickYourselfError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\terr = h.guildService.RemoveMember(req.MemberId, guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to kick member: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit signals to remove the member from the guild\n\th.socketService.EmitRemoveMember(guild.ID, member.ID)\n\th.socketService.EmitRemoveFromGuild(member.ID, guildId)\n\n\tc.JSON(http.StatusOK, true)\n}\n"
  },
  {
    "path": "server/handler/member_handler_test.go",
    "content": "package handler\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n)\n\nfunc TestHandler_GetMemberSettings(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\tmockGuild := fixture.GetMockGuild(\"\")\n\n\tsettings := &model.MemberSettings{\n\t\tNickname: nil,\n\t\tColor:    nil,\n\t}\n\n\tt.Run(\"Successfully fetched settings\", func(t *testing.T) {\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tauthUser.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"GetMemberSettings\", mockArgs...).Return(settings, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(settings)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetMemberSettings\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", id)\n\t\tmockGuildService.On(\"GetGuild\", id).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", id)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"GetMemberSettings\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockError := apperrors.NewNotFound(\"user\", authUser.ID)\n\t\tmockArgs := mock.Arguments{\n\t\t\tauthUser.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"GetMemberSettings\", mockArgs...).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_EditMemberSettings(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\n\tauthUser := fixture.GetMockUser()\n\tnickname := fixture.Username()\n\tcolor := \"#fff\"\n\n\tsettings := &model.MemberSettings{\n\t\tNickname: &nickname,\n\t\tColor:    &color,\n\t}\n\n\tt.Run(\"Successfully edited settings\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tsettings,\n\t\t\tauthUser.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"UpdateMemberSettings\", mockArgs...).Return(nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"nickname\": nickname,\n\t\t\t\"color\":    color,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Successfully reset member settings\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockArgs := mock.Arguments{\n\t\t\t&model.MemberSettings{},\n\t\t\tauthUser.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"UpdateMemberSettings\", mockArgs...).Return(nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"nickname\": nil,\n\t\t\t\"color\":    nil,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Not a member of the server\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"nickname\": nickname,\n\t\t\t\"color\":    color,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", mockGuild.ID)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateMemberSettings\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateMemberSettings\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\tmockError := apperrors.NewNotFound(\"guild\", id)\n\t\tmockGuildService.On(\"GetGuild\", id).Return(nil, mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"nickname\": nickname,\n\t\t\t\"color\":    color,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", id)\n\t\trequest, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", id)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateMemberSettings\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *authUser)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockArgs := mock.Arguments{\n\t\t\tsettings,\n\t\t\tauthUser.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"UpdateMemberSettings\", mockArgs...).Return(mockError)\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"nickname\": nickname,\n\t\t\t\"color\":    color,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_EditMemberSettings_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\trouter := getAuthenticatedTestRouter(mockUser.ID)\n\n\tmockGuildService := new(mocks.GuildService)\n\n\tNewHandler(&Config{\n\t\tR:            router,\n\t\tGuildService: mockGuildService,\n\t})\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody gin.H\n\t}{\n\t\t{\n\t\t\tname: \"Nickname too short\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"nickname\": fixture.RandStringRunes(2),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Nickname too long\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"nickname\": fixture.RandStringRunes(32),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Color not a hex color\",\n\t\t\tbody: gin.H{\n\t\t\t\t\"color\": fixture.RandStringRunes(6),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\treqBody, err := json.Marshal(tc.body)\n\t\t\tassert.NoError(t, err)\n\n\t\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/member\", fixture.RandID())\n\t\t\trequest, err := http.NewRequest(http.MethodPut, reqUrl, bytes.NewBuffer(reqBody))\n\t\t\tassert.NoError(t, err)\n\n\t\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockGuildService.AssertNotCalled(t, \"UpdateMemberSettings\")\n\t\t})\n\t}\n}\n\nfunc TestHandler_GetBanList(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful Fetch\", func(t *testing.T) {\n\t\tresponse := make([]model.BanResponse, 0)\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tmockUser := fixture.GetMockUser()\n\t\t\tresponse = append(response, model.BanResponse{\n\t\t\t\tId:       mockUser.ID,\n\t\t\t\tUsername: mockUser.Username,\n\t\t\t\tImage:    mockUser.Image,\n\t\t\t})\n\t\t}\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"GetBanList\", mockGuild.ID).Return(&response, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(response)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Not the owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockGuildService.AssertNotCalled(t, \"GetBanList\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetBanList\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockGuildService.On(\"GetBanList\", mockGuild.ID).Return(nil, mockError)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodGet, reqUrl, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_BanMember(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful Ban\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"GetUser\", mockMember.ID).Return(mockMember, nil)\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(nil)\n\n\t\targs := mock.Arguments{\n\t\t\tmockMember.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"RemoveMember\", args...).Return(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitRemoveMember\", mockGuild.ID, mockMember.ID)\n\t\tmockSocketService.On(\"EmitRemoveFromGuild\", mockMember.ID, mockGuild.ID)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Not the owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"GetUser\", mockMember.ID).Return(mockMember, nil)\n\t\tmockGuildService.On(\"UpdateGuild\", mockGuild).Return(nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\targs := mock.Arguments{\n\t\t\tmockMember.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"RemoveMember\", args...).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockError := apperrors.NewNotFound(\"guild\", mockGuild.ID)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"Member not found\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockError := apperrors.NewNotFound(\"user\", mockMember.ID)\n\t\tmockGuildService.On(\"GetUser\", mockMember.ID).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetUser\", mockMember.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"MemberId required\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"UpdateGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"MemberId and AuthUserId are equal\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": authUser.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.BanYourselfError)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetUser\", authUser.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n}\n\nfunc TestHandler_KickMember(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful Kick\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"GetUser\", mockMember.ID).Return(mockMember, nil)\n\n\t\targs := mock.Arguments{\n\t\t\tmockMember.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"RemoveMember\", args...).Return(nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitRemoveMember\", mockGuild.ID, mockMember.ID)\n\t\tmockSocketService.On(\"EmitRemoveFromGuild\", mockMember.ID, mockGuild.ID)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Not the owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"GetUser\", mockMember.ID).Return(mockMember, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\targs := mock.Arguments{\n\t\t\tmockMember.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"RemoveMember\", args...).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockError := apperrors.NewNotFound(\"guild\", mockGuild.ID)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"Member not found\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockError := apperrors.NewNotFound(\"user\", mockMember.ID)\n\t\tmockGuildService.On(\"GetUser\", mockMember.ID).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetUser\", mockMember.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"MemberId required\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetUser\")\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n\n\tt.Run(\"MemberId and AuthUserId are equal\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\t\tmockGuildService.On(\"GetUser\", authUser.ID).Return(authUser, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": authUser.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/kick\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.KickYourselfError)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetUser\", authUser.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"RemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveMember\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitRemoveFromGuild\")\n\t})\n}\n\nfunc TestHandler_UnbanMember(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful Unban\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\targs := mock.Arguments{\n\t\t\tmockMember.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"UnbanMember\", args...).Return(nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Not the owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.MustBeOwner)\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UnbanMember\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"UnbanMember\")\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockError := apperrors.NewInternal()\n\t\targs := mock.Arguments{\n\t\t\tmockMember.ID,\n\t\t\tmockGuild.ID,\n\t\t}\n\t\tmockGuildService.On(\"UnbanMember\", args...).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:             router,\n\t\t\tGuildService:  mockGuildService,\n\t\t\tSocketService: mockSocketService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockGuildService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Guild not found\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockMember := fixture.GetMockUser()\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockError := apperrors.NewNotFound(\"guild\", mockGuild.ID)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(nil, mockError)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": mockMember.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UnbanMember\")\n\t})\n\n\tt.Run(\"MemberId required\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockGuildService.AssertNotCalled(t, \"UnbanMember\")\n\t})\n\n\tt.Run(\"MemberId and AuthUserId are equal\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:            router,\n\t\t\tGuildService: mockGuildService,\n\t\t})\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"memberId\": authUser.ID,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\treqUrl := fmt.Sprintf(\"/api/guilds/%s/bans\", mockGuild.ID)\n\t\trequest, err := http.NewRequest(http.MethodDelete, reqUrl, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewBadRequest(apperrors.UnbanYourselfError)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockGuildService.AssertNotCalled(t, \"UnbanMember\")\n\t})\n}\n"
  },
  {
    "path": "server/handler/message_handler.go",
    "content": "package handler\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-gonic/gin\"\n\tvalidation \"github.com/go-ozzo/ozzo-validation/v4\"\n\tgonanoid \"github.com/matoous/go-nanoid\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\n/*\n * MessageHandler contains all routes related to message actions (/api/messages)\n */\n\n// GetMessages returns messages for the given channel\n// It returns the most recent 35 or the ones after the given cursor\n// GetMessages godoc\n// @Tags Messages\n// @Summary Get Channel Messages\n// @Produce  json\n// @Param channelId path string true \"Channel ID\"\n// @Param cursor query string false \"Cursor Pagination using the createdAt field\"\n// @Success 200 {array} model.MessageResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Router /messages/{channelId} [get]\nfunc (h *Handler) GetMessages(c *gin.Context) {\n\tchannelId := c.Param(\"channelId\")\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tchannel, err := h.channelService.Get(channelId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"channel\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the user has access to said channel\n\terr = h.channelService.IsChannelMember(channel, userId)\n\n\tif err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Cursor is based on the created_at field of the message\n\tcursor := c.Query(\"cursor\")\n\n\tmessages, err := h.messageService.GetMessages(userId, channel, cursor)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"messages\", channelId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// If the channel does not have any messages, return an empty array\n\tif len(*messages) == 0 {\n\t\tvar empty = make([]model.MessageResponse, 0)\n\t\tc.JSON(http.StatusOK, empty)\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, messages)\n}\n\n// messageRequest contains all field required to create a message.\n// Either text or file must be provided\ntype messageRequest struct {\n\t// Maximum 2000 characters\n\tText *string `form:\"text\"`\n\t// image/* or audio/*\n\tFile *multipart.FileHeader `form:\"file\" swaggertype:\"string\" format:\"binary\"`\n} //@name MessageRequest\n\nfunc (r messageRequest) validate() error {\n\treturn validation.ValidateStruct(&r,\n\t\tvalidation.Field(&r.Text,\n\t\t\tvalidation.NilOrNotEmpty,\n\t\t\tvalidation.Required.When(r.File == nil).\n\t\t\t\tError(apperrors.MessageOrFileRequired),\n\t\t\tvalidation.Length(1, 2000),\n\t\t),\n\t)\n}\n\nfunc (r *messageRequest) sanitize() {\n\tif r.Text != nil {\n\t\ttext := strings.TrimSpace(*r.Text)\n\t\tr.Text = &text\n\t}\n}\n\n// CreateMessage creates a message in the given channel\n// CreateMessage godoc\n// @Tags Messages\n// @Summary Create Messages\n// @Accepts  mpfd\n// @Produce  json\n// @Param channelId path string true \"Channel ID\"\n// @Param request body messageRequest true \"Create Message\"\n// @Success 201 {object} model.Success\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /messages/{channelId} [post]\nfunc (h *Handler) CreateMessage(c *gin.Context) {\n\tchannelId := c.Param(\"channelId\")\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tvar req messageRequest\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\treq.sanitize()\n\n\tchannel, err := h.channelService.Get(channelId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"channel\", channelId)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if the user has access to said channel\n\terr = h.channelService.IsChannelMember(channel, userId)\n\n\tif err != nil {\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tauthor, err := h.userService.Get(userId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"user\", userId)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tparams := model.Message{\n\t\tUserId:    userId,\n\t\tChannelId: channel.ID,\n\t}\n\n\tparams.Text = req.Text\n\n\tif req.File != nil {\n\t\tmimeType := req.File.Header.Get(\"Content-Type\")\n\n\t\tif valid := isAllowedFileType(mimeType); !valid {\n\t\t\ttoFieldErrorResponse(c, \"File\", apperrors.InvalidImageType)\n\t\t\treturn\n\t\t}\n\n\t\t// Prevent file upload on the live server.\n\t\t// Remove the if part if you do want upload\n\t\tvar attachment *model.Attachment\n\t\tif gin.Mode() == gin.ReleaseMode {\n\t\t\tid, _ := gonanoid.Nanoid(20)\n\n\t\t\t// Random image to test files in the app\n\t\t\tattachment = &model.Attachment{\n\t\t\t\tID:       id,\n\t\t\t\tUrl:      fmt.Sprintf(\"https://picsum.photos/seed/%s/600\", id),\n\t\t\t\tFileType: \"image/jpeg\",\n\t\t\t\tFilename: id,\n\t\t\t}\n\t\t} else {\n\t\t\tattachment, err = h.messageService.UploadFile(req.File, channel.ID)\n\n\t\t\tif err != nil {\n\t\t\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\t\t\"error\": err,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tparams.Attachment = attachment\n\t}\n\n\tmessage, err := h.messageService.CreateMessage(&params)\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to create message: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tresponse := model.MessageResponse{\n\t\tId:         message.ID,\n\t\tText:       message.Text,\n\t\tCreatedAt:  message.CreatedAt,\n\t\tUpdatedAt:  message.UpdatedAt,\n\t\tAttachment: message.Attachment,\n\t\tUser: model.MemberResponse{\n\t\t\tId:        author.ID,\n\t\t\tUsername:  author.Username,\n\t\t\tImage:     author.Image,\n\t\t\tIsOnline:  author.IsOnline,\n\t\t\tCreatedAt: author.CreatedAt,\n\t\t\tUpdatedAt: author.UpdatedAt,\n\t\t\tIsFriend:  false,\n\t\t},\n\t}\n\n\t// Get member settings if it is not a DM\n\tif !channel.IsDM {\n\t\tsettings, _ := h.guildService.GetMemberSettings(userId, *channel.GuildID)\n\t\tresponse.User.Nickname = settings.Nickname\n\t\tresponse.User.Color = settings.Color\n\t}\n\n\t// Emit new message to the channel\n\th.socketService.EmitNewMessage(channelId, &response)\n\n\tif channel.IsDM {\n\t\t// Open the DM and push it to the top\n\t\t_ = h.channelService.OpenDMForAll(channelId)\n\t\t// Post a notification\n\t\th.socketService.EmitNewDMNotification(channelId, author)\n\t} else {\n\t\t// Update last activity in channel\n\t\tchannel.LastActivity = time.Now()\n\t\t_ = h.channelService.UpdateChannel(channel)\n\t\t// Post a notification\n\t\th.socketService.EmitNewNotification(*channel.GuildID, channelId)\n\t}\n\n\tc.JSON(http.StatusCreated, true)\n}\n\n// EditMessage edits the given message with the given text\n// EditMessage godoc\n// @Tags Messages\n// @Summary Edit Messages\n// @Accepts  json\n// @Produce  json\n// @Param messageId path string true \"Message ID\"\n// @Param request body messageRequest true \"Edit Message\"\n// @Success 200 {object} model.Success\n// @Failure 400 {object} model.ErrorsResponse\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /messages/{messageId} [put]\nfunc (h *Handler) EditMessage(c *gin.Context) {\n\tmessageId := c.Param(\"messageId\")\n\tuserId := c.MustGet(\"userId\").(string)\n\n\tvar req messageRequest\n\t// Bind incoming json to struct and check for validation errors\n\tif ok := bindData(c, &req); !ok {\n\t\treturn\n\t}\n\n\tmessage, err := h.messageService.Get(messageId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"message\", messageId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tif message.UserId != userId {\n\t\te := apperrors.NewAuthorization(apperrors.EditMessageError)\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tmessage.Text = req.Text\n\n\tif err = h.messageService.UpdateMessage(message); err != nil {\n\t\tlog.Printf(\"Failed to edit message: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\tresponse := model.MessageResponse{\n\t\tId:         message.ID,\n\t\tText:       message.Text,\n\t\tCreatedAt:  message.CreatedAt,\n\t\tUpdatedAt:  message.UpdatedAt,\n\t\tAttachment: message.Attachment,\n\t\tUser: model.MemberResponse{\n\t\t\tId: userId,\n\t\t},\n\t}\n\n\t// Emit edited message to the channel\n\th.socketService.EmitEditMessage(message.ChannelId, &response)\n\n\tc.JSON(http.StatusOK, true)\n}\n\n// DeleteMessage deletes the given message\n// DeleteMessage godoc\n// @Tags Messages\n// @Summary Delete Messages\n// @Produce  json\n// @Param messageId path string true \"Message ID\"\n// @Success 200 {object} model.Success\n// @Failure 401 {object} model.ErrorResponse\n// @Failure 404 {object} model.ErrorResponse\n// @Failure 500 {object} model.ErrorResponse\n// @Router /messages/{messageId} [delete]\nfunc (h *Handler) DeleteMessage(c *gin.Context) {\n\tmessageId := c.Param(\"messageId\")\n\tuserId := c.MustGet(\"userId\").(string)\n\tmessage, err := h.messageService.Get(messageId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"message\", messageId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\tchannel, err := h.channelService.Get(message.ChannelId)\n\n\tif err != nil {\n\t\te := apperrors.NewNotFound(\"message\", messageId)\n\n\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\"error\": e,\n\t\t})\n\t\treturn\n\t}\n\n\t// Check if message author or guild owner\n\tif !channel.IsDM {\n\t\tguild, err := h.guildService.GetGuild(*channel.GuildID)\n\n\t\tif err != nil {\n\t\t\te := apperrors.NewNotFound(\"message\", messageId)\n\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tif message.UserId != userId && guild.OwnerId != userId {\n\t\t\te := apperrors.NewAuthorization(apperrors.DeleteMessageError)\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t\t// Only message author check required\n\t} else {\n\t\tif message.UserId != userId {\n\t\t\te := apperrors.NewAuthorization(apperrors.DeleteDMMessageError)\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err = h.messageService.DeleteMessage(message); err != nil {\n\t\tlog.Printf(\"Failed to delete message: %v\\n\", err.Error())\n\t\tc.JSON(apperrors.Status(err), gin.H{\n\t\t\t\"error\": err,\n\t\t})\n\t\treturn\n\t}\n\n\t// Emit delete message to the channel\n\th.socketService.EmitDeleteMessage(message.ChannelId, message.ID)\n\n\tc.JSON(http.StatusOK, true)\n}\n"
  },
  {
    "path": "server/handler/message_handler_test.go",
    "content": "package handler\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestHandler_GetMessages(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successful fetch\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(fixture.RandID())\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(nil)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\n\t\targs := mock.Arguments{\n\t\t\tauthUser.ID,\n\t\t\tmockChannel,\n\t\t\t\"\",\n\t\t}\n\n\t\tresponse := make([]model.MessageResponse, 0)\n\n\t\tfor i := 0; i < 25; i++ {\n\t\t\tmessage := fixture.GetMockMessageResponse(\"\", mockChannel.ID)\n\t\t\tresponse = append(response, *message)\n\t\t}\n\n\t\tmockMessageService.On(\"GetMessages\", args...).Return(&response, nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/messages/\"+mockChannel.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(response)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockMessageService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"No channel found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"channel\", id)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", id).Return(nil, mockError)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/messages/\"+id, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", id)\n\t\tmockChannelService.AssertNotCalled(t, \"IsChannelMember\")\n\t\tmockMessageService.AssertNotCalled(t, \"GetMessages\")\n\t})\n\n\tt.Run(\"Not a member of the channel\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(fixture.RandID())\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.Unauthorized)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(mockError)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/messages/\"+mockChannel.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockMessageService.AssertNotCalled(t, \"GetMessages\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockMessageService := new(mocks.MessageService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/messages/\"+id, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertNotCalled(t, \"Get\")\n\t\tmockChannelService.AssertNotCalled(t, \"IsChannelMember\")\n\t\tmockMessageService.AssertNotCalled(t, \"GetMessages\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(fixture.RandID())\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(nil)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\n\t\targs := mock.Arguments{\n\t\t\tauthUser.ID,\n\t\t\tmockChannel,\n\t\t\t\"\",\n\t\t}\n\t\tmockError := apperrors.NewNotFound(\"messages\", mockChannel.ID)\n\t\tmockMessageService.On(\"GetMessages\", args...).Return(nil, mockError)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t})\n\n\t\trequest, err := http.NewRequest(http.MethodGet, \"/api/messages/\"+mockChannel.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockMessageService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_CreateMessage(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully created text message\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(nil)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", authUser.ID).Return(authUser, nil)\n\n\t\tparams := model.Message{\n\t\t\tUserId:    mockMessage.UserId,\n\t\t\tChannelId: mockMessage.ChannelId,\n\t\t\tText:      mockMessage.Text,\n\t\t}\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"CreateMessage\", &params).Return(mockMessage, nil)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetMemberSettings\", authUser.ID, mockGuild.ID).Return(&model.MemberSettings{}, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tresponse := model.MessageResponse{\n\t\t\tId:         mockMessage.ID,\n\t\t\tText:       mockMessage.Text,\n\t\t\tCreatedAt:  mockMessage.CreatedAt,\n\t\t\tUpdatedAt:  mockMessage.UpdatedAt,\n\t\t\tAttachment: mockMessage.Attachment,\n\t\t\tUser: model.MemberResponse{\n\t\t\t\tId:        authUser.ID,\n\t\t\t\tUsername:  authUser.Username,\n\t\t\t\tImage:     authUser.Image,\n\t\t\t\tIsOnline:  authUser.IsOnline,\n\t\t\t\tCreatedAt: authUser.CreatedAt,\n\t\t\t\tUpdatedAt: authUser.UpdatedAt,\n\t\t\t\tIsFriend:  false,\n\t\t\t},\n\t\t}\n\n\t\tmockSocketService.On(\"EmitNewMessage\", mockChannel.ID, &response).Return()\n\t\tmockChannelService.On(\"UpdateChannel\", mockChannel).Return(nil)\n\t\tmockSocketService.On(\"EmitNewNotification\", mockGuild.ID, mockChannel.ID)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tSocketService:  mockSocketService,\n\t\t\tUserService:    mockUserService,\n\t\t})\n\n\t\tform := url.Values{}\n\t\tform.Add(\"text\", *mockMessage.Text)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/messages/\"+mockChannel.ID, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusCreated, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t\tmockUserService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Channel not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"channel\", id)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", id).Return(nil, mockError)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tSocketService:  mockSocketService,\n\t\t\tUserService:    mockUserService,\n\t\t})\n\n\t\tform := url.Values{}\n\t\tform.Add(\"text\", fixture.RandStringRunes(8))\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/messages/\"+id, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", id)\n\t\tmockChannelService.AssertNotCalled(t, \"IsChannelMember\")\n\t\tmockUserService.AssertNotCalled(t, \"Get\")\n\t\tmockMessageService.AssertNotCalled(t, \"CreateMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewMessage\")\n\t})\n\n\tt.Run(\"Not a member of the channel\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(\"\")\n\t\tmockError := apperrors.NewAuthorization(apperrors.Unauthorized)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(mockError)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tSocketService:  mockSocketService,\n\t\t\tUserService:    mockUserService,\n\t\t})\n\n\t\tform := url.Values{}\n\t\tform.Add(\"text\", fixture.RandStringRunes(8))\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/messages/\"+mockChannel.ID, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockChannelService.AssertCalled(t, \"IsChannelMember\", mockChannel, authUser.ID)\n\t\tmockUserService.AssertNotCalled(t, \"Get\")\n\t\tmockMessageService.AssertNotCalled(t, \"CreateMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewMessage\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tSocketService:  mockSocketService,\n\t\t\tUserService:    mockUserService,\n\t\t})\n\n\t\tform := url.Values{}\n\t\tform.Add(\"text\", fixture.RandStringRunes(8))\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/messages/\"+id, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertNotCalled(t, \"Get\")\n\t\tmockChannelService.AssertNotCalled(t, \"IsChannelMember\")\n\t\tmockUserService.AssertNotCalled(t, \"Get\")\n\t\tmockMessageService.AssertNotCalled(t, \"CreateMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewMessage\")\n\t})\n\n\tt.Run(\"Text Message Creation failure\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(nil)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", authUser.ID).Return(authUser, nil)\n\n\t\tparams := model.Message{\n\t\t\tUserId:    mockMessage.UserId,\n\t\t\tChannelId: mockMessage.ChannelId,\n\t\t\tText:      mockMessage.Text,\n\t\t}\n\t\tmockError := apperrors.NewInternal()\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"CreateMessage\", &params).Return(nil, mockError)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tSocketService:  mockSocketService,\n\t\t\tUserService:    mockUserService,\n\t\t})\n\n\t\tform := url.Values{}\n\t\tform.Add(\"text\", *mockMessage.Text)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/messages/\"+mockChannel.ID, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockChannelService.AssertCalled(t, \"IsChannelMember\", mockChannel, authUser.ID)\n\t\tmockMessageService.AssertCalled(t, \"CreateMessage\", &params)\n\t\tmockUserService.AssertCalled(t, \"Get\", authUser.ID)\n\t\tmockChannelService.AssertNotCalled(t, \"UpdateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewMessage\")\n\t})\n\n\tt.Run(\"Disallowed mimetype\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(nil)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", authUser.ID).Return(authUser, nil)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tSocketService:  mockSocketService,\n\t\t\tUserService:    mockUserService,\n\t\t})\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.txt\", \"image/txt\")\n\t\tdefer multipartImageFixture.Close()\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/messages/\"+mockChannel.ID, multipartImageFixture.MultipartBody)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", multipartImageFixture.ContentType)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockChannelService.AssertCalled(t, \"IsChannelMember\", mockChannel, authUser.ID)\n\t\tmockUserService.AssertCalled(t, \"Get\", authUser.ID)\n\t\tmockMessageService.AssertNotCalled(t, \"UploadFile\")\n\t\tmockMessageService.AssertNotCalled(t, \"CreateMessage\")\n\t\tmockChannelService.AssertNotCalled(t, \"UpdateChannel\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitNewMessage\")\n\t})\n\n\tt.Run(\"Image Message Creation Success\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID)\n\t\tmockMessage.Text = nil\n\n\t\tuploadImageFixture := fixture.NewMultipartImage(\"image.png\", \"image/png\")\n\t\tdefer uploadImageFixture.Close()\n\t\tformFile := uploadImageFixture.GetFormFile()\n\n\t\tattachment := &model.Attachment{\n\t\t\tID:        fixture.RandID(),\n\t\t\tCreatedAt: time.Now(),\n\t\t\tUpdatedAt: time.Now(),\n\t\t\tUrl:       fixture.RandStringRunes(8),\n\t\t\tFileType:  \"image/png\",\n\t\t\tFilename:  fixture.RandStringRunes(8),\n\t\t\tMessageId: mockMessage.ID,\n\t\t}\n\t\tmockMessage.Attachment = attachment\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(nil)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", authUser.ID).Return(authUser, nil)\n\n\t\tparams := model.Message{\n\t\t\tUserId:     mockMessage.UserId,\n\t\t\tChannelId:  mockMessage.ChannelId,\n\t\t\tAttachment: attachment,\n\t\t}\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"UploadFile\", formFile, mockChannel.ID).Return(attachment, nil)\n\t\tmockMessageService.On(\"CreateMessage\", &params).Return(mockMessage, nil)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetMemberSettings\", authUser.ID, mockGuild.ID).Return(&model.MemberSettings{}, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tresponse := model.MessageResponse{\n\t\t\tId:         mockMessage.ID,\n\t\t\tText:       nil,\n\t\t\tCreatedAt:  mockMessage.CreatedAt,\n\t\t\tUpdatedAt:  mockMessage.UpdatedAt,\n\t\t\tAttachment: attachment,\n\t\t\tUser: model.MemberResponse{\n\t\t\t\tId:        authUser.ID,\n\t\t\t\tUsername:  authUser.Username,\n\t\t\t\tImage:     authUser.Image,\n\t\t\t\tIsOnline:  authUser.IsOnline,\n\t\t\t\tCreatedAt: authUser.CreatedAt,\n\t\t\t\tUpdatedAt: authUser.UpdatedAt,\n\t\t\t\tIsFriend:  false,\n\t\t\t},\n\t\t}\n\n\t\tmockSocketService.On(\"EmitNewMessage\", mockChannel.ID, &response).Return()\n\t\tmockChannelService.On(\"UpdateChannel\", mockChannel).Return(nil)\n\t\tmockSocketService.On(\"EmitNewNotification\", mockGuild.ID, mockChannel.ID)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tSocketService:  mockSocketService,\n\t\t\tUserService:    mockUserService,\n\t\t})\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.png\", \"image/png\")\n\t\tdefer multipartImageFixture.Close()\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/messages/\"+mockChannel.ID, multipartImageFixture.MultipartBody)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", multipartImageFixture.ContentType)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusCreated, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t\tmockUserService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"DM channel message success\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(\"\")\n\t\tmockChannel.IsDM = true\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\t\tmockChannelService.On(\"IsChannelMember\", mockChannel, authUser.ID).Return(nil)\n\n\t\tmockUserService := new(mocks.UserService)\n\t\tmockUserService.On(\"Get\", authUser.ID).Return(authUser, nil)\n\n\t\tparams := model.Message{\n\t\t\tUserId:    mockMessage.UserId,\n\t\t\tChannelId: mockMessage.ChannelId,\n\t\t\tText:      mockMessage.Text,\n\t\t}\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"CreateMessage\", &params).Return(mockMessage, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tresponse := model.MessageResponse{\n\t\t\tId:         mockMessage.ID,\n\t\t\tText:       mockMessage.Text,\n\t\t\tCreatedAt:  mockMessage.CreatedAt,\n\t\t\tUpdatedAt:  mockMessage.UpdatedAt,\n\t\t\tAttachment: mockMessage.Attachment,\n\t\t\tUser: model.MemberResponse{\n\t\t\t\tId:        authUser.ID,\n\t\t\t\tUsername:  authUser.Username,\n\t\t\t\tImage:     authUser.Image,\n\t\t\t\tIsOnline:  authUser.IsOnline,\n\t\t\t\tCreatedAt: authUser.CreatedAt,\n\t\t\t\tUpdatedAt: authUser.UpdatedAt,\n\t\t\t\tIsFriend:  false,\n\t\t\t},\n\t\t}\n\n\t\tmockSocketService.On(\"EmitNewMessage\", mockChannel.ID, &response).Return()\n\t\tmockSocketService.On(\"EmitNewDMNotification\", mockChannel.ID, authUser).Return()\n\t\tmockChannelService.On(\"OpenDMForAll\", mockChannel.ID).Return(nil)\n\n\t\trr := httptest.NewRecorder()\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tChannelService: mockChannelService,\n\t\t\tMessageService: mockMessageService,\n\t\t\tSocketService:  mockSocketService,\n\t\t\tUserService:    mockUserService,\n\t\t})\n\n\t\tform := url.Values{}\n\t\tform.Add(\"text\", *mockMessage.Text)\n\n\t\trequest, err := http.NewRequest(http.MethodPost, \"/api/messages/\"+mockChannel.ID, strings.NewReader(form.Encode()))\n\t\tassert.NoError(t, err)\n\t\trequest.Form = form\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\trespBody, err := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, http.StatusCreated, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t\tmockUserService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_CreateMessage_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\trouter := getAuthenticatedTestRouter(mockUser.ID)\n\n\tmockMessageService := new(mocks.MessageService)\n\n\tNewHandler(&Config{\n\t\tR:              router,\n\t\tMessageService: mockMessageService,\n\t\tMaxBodyBytes:   4 * 1024 * 1024,\n\t})\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody url.Values\n\t}{\n\t\t{\n\t\t\tname: \"No file nor text\",\n\t\t\tbody: map[string][]string{},\n\t\t},\n\t\t{\n\t\t\tname: \"Text empty and no file\",\n\t\t\tbody: map[string][]string{\n\t\t\t\t\"text\": {\"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Text too long\",\n\t\t\tbody: map[string][]string{\n\t\t\t\t\"text\": {fixture.RandStringRunes(2001)},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\tform := tc.body\n\t\t\trequest, _ := http.NewRequest(http.MethodPost, \"/api/messages/\"+fixture.RandID(), strings.NewReader(form.Encode()))\n\t\t\trequest.Form = form\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockMessageService.AssertNotCalled(t, \"CreateMessage\")\n\t\t})\n\t}\n}\n\nfunc TestHandler_UpdateMessage(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully updated\", func(t *testing.T) {\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, \"\")\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\t\tmockMessageService.On(\"UpdateMessage\", mockMessage).Return(nil)\n\n\t\tresponse := model.MessageResponse{\n\t\t\tId:         mockMessage.ID,\n\t\t\tText:       mockMessage.Text,\n\t\t\tCreatedAt:  mockMessage.CreatedAt,\n\t\t\tUpdatedAt:  mockMessage.UpdatedAt,\n\t\t\tAttachment: mockMessage.Attachment,\n\t\t\tUser: model.MemberResponse{\n\t\t\t\tId: authUser.ID,\n\t\t\t},\n\t\t}\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitEditMessage\", mockMessage.ChannelId, &response).Return()\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"text\": *mockMessage.Text,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/messages/\"+mockMessage.ID, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Message not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"message\", id)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", id).Return(nil, mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"text\": fixture.RandStringRunes(12),\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/messages/\"+id, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertCalled(t, \"Get\", id)\n\t\tmockMessageService.AssertNotCalled(t, \"UpdateMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditMessage\")\n\t})\n\n\tt.Run(\"Not the message author\", func(t *testing.T) {\n\t\tmockMessage := fixture.GetMockMessage(\"\", \"\")\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"text\": fixture.RandStringRunes(12),\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/messages/\"+mockMessage.ID, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.EditMessageError)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertCalled(t, \"Get\", mockMessage.ID)\n\t\tmockMessageService.AssertNotCalled(t, \"UpdateMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditMessage\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockMessage := fixture.GetMockMessage(\"\", \"\")\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"text\": fixture.RandStringRunes(12),\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/messages/\"+mockMessage.ID, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertNotCalled(t, \"Get\")\n\t\tmockMessageService.AssertNotCalled(t, \"UpdateMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitEditMessage\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, \"\")\n\t\tmockError := apperrors.NewInternal()\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\t\tmockMessageService.On(\"UpdateMessage\", mockMessage).Return(mockError)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\treqBody, err := json.Marshal(gin.H{\n\t\t\t\"text\": *mockMessage.Text,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodPut, \"/api/messages/\"+mockMessage.ID, bytes.NewBuffer(reqBody))\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n}\n\nfunc TestHandler_UpdateMessage_BadRequest(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\n\tmockUser := fixture.GetMockUser()\n\trouter := getAuthenticatedTestRouter(mockUser.ID)\n\n\tmockMessageService := new(mocks.MessageService)\n\n\tNewHandler(&Config{\n\t\tR:              router,\n\t\tMessageService: mockMessageService,\n\t\tMaxBodyBytes:   4 * 1024 * 1024,\n\t})\n\n\ttestCases := []struct {\n\t\tname string\n\t\tbody url.Values\n\t}{\n\t\t{\n\t\t\tname: \"Text is required\",\n\t\t\tbody: map[string][]string{},\n\t\t},\n\t\t{\n\t\t\tname: \"Text empty\",\n\t\t\tbody: map[string][]string{\n\t\t\t\t\"text\": {\"\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"Text too long\",\n\t\t\tbody: map[string][]string{\n\t\t\t\t\"text\": {fixture.RandStringRunes(2001)},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor i := range testCases {\n\t\ttc := testCases[i]\n\n\t\tt.Run(tc.name, func(t *testing.T) {\n\n\t\t\trr := httptest.NewRecorder()\n\n\t\t\tform := tc.body\n\t\t\trequest, _ := http.NewRequest(http.MethodPut, \"/api/messages/\"+fixture.RandID(), strings.NewReader(form.Encode()))\n\t\t\trequest.Form = form\n\n\t\t\trouter.ServeHTTP(rr, request)\n\n\t\t\tassert.Equal(t, http.StatusBadRequest, rr.Code)\n\t\t\tmockMessageService.AssertNotCalled(t, \"UpdateMessage\")\n\t\t})\n\t}\n}\n\nfunc TestHandler_Delete_Message(t *testing.T) {\n\t// Setup\n\tgin.SetMode(gin.TestMode)\n\tauthUser := fixture.GetMockUser()\n\n\tt.Run(\"Successfully deleted\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\t\tmockMessageService.On(\"DeleteMessage\", mockMessage).Return(nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitDeleteMessage\", mockChannel.ID, mockMessage.ID)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+mockMessage.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Message not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockError := apperrors.NewNotFound(\"message\", id)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", id).Return(nil, mockError)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+id, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertCalled(t, \"Get\", id)\n\t\tmockMessageService.AssertNotCalled(t, \"DeleteMessage\")\n\t\tmockChannelService.AssertNotCalled(t, \"Get\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteMessage\")\n\t})\n\n\tt.Run(\"Channel not found\", func(t *testing.T) {\n\t\tid := fixture.RandID()\n\t\tmockMessage := fixture.GetMockMessage(\"\", id)\n\t\tmockError := apperrors.NewNotFound(\"message\", mockMessage.ID)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", id).Return(nil, mockError)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+mockMessage.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertCalled(t, \"Get\", mockMessage.ID)\n\t\tmockChannelService.AssertCalled(t, \"Get\", id)\n\t\tmockMessageService.AssertNotCalled(t, \"DeleteMessage\")\n\t\tmockGuildService.AssertNotCalled(t, \"GetGuild\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteMessage\")\n\t})\n\n\tt.Run(\"Delete in guild - guild owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(authUser.ID)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockMessage := fixture.GetMockMessage(\"\", mockChannel.ID)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\t\tmockMessageService.On(\"DeleteMessage\", mockMessage).Return(nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitDeleteMessage\", mockChannel.ID, mockMessage.ID)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+mockMessage.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Delete in guild - not the guild owner\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockMessage := fixture.GetMockMessage(\"\", mockChannel.ID)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+mockMessage.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.DeleteMessageError)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertCalled(t, \"Get\", mockMessage.ID)\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockGuildService.AssertCalled(t, \"GetGuild\", mockGuild.ID)\n\t\tmockMessageService.AssertNotCalled(t, \"DeleteMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteMessage\")\n\t})\n\n\tt.Run(\"Delete in DM - Author\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockDMChannel()\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\t\tmockMessageService.On(\"DeleteMessage\", mockMessage).Return(nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\t\tmockSocketService.On(\"EmitDeleteMessage\", mockChannel.ID, mockMessage.ID)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+mockMessage.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(true)\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockSocketService.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Delete in DM - Not the author\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockDMChannel()\n\t\tmockMessage := fixture.GetMockMessage(\"\", mockChannel.ID)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\t\tmockMessageService.On(\"DeleteMessage\", mockMessage).Return(nil)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+mockMessage.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.DeleteDMMessageError)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertCalled(t, \"Get\", mockMessage.ID)\n\t\tmockChannelService.AssertCalled(t, \"Get\", mockChannel.ID)\n\t\tmockMessageService.AssertNotCalled(t, \"DeleteMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteMessage\")\n\t})\n\n\tt.Run(\"Unauthorized\", func(t *testing.T) {\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getTestRouter()\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+fixture.RandID(), nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, mockError.Status(), rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertNotCalled(t, \"Get\")\n\t\tmockChannelService.AssertNotCalled(t, \"Get\")\n\t\tmockMessageService.AssertNotCalled(t, \"DeleteMessage\")\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteMessage\")\n\t})\n\n\tt.Run(\"Server Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\t\tmockMessage := fixture.GetMockMessage(authUser.ID, mockChannel.ID)\n\n\t\tmockMessageService := new(mocks.MessageService)\n\t\tmockMessageService.On(\"Get\", mockMessage.ID).Return(mockMessage, nil)\n\t\tmockError := apperrors.NewInternal()\n\t\tmockMessageService.On(\"DeleteMessage\", mockMessage).Return(mockError)\n\n\t\tmockChannelService := new(mocks.ChannelService)\n\t\tmockChannelService.On(\"Get\", mockChannel.ID).Return(mockChannel, nil)\n\n\t\tmockGuildService := new(mocks.GuildService)\n\t\tmockGuildService.On(\"GetGuild\", mockGuild.ID).Return(mockGuild, nil)\n\n\t\tmockSocketService := new(mocks.SocketService)\n\n\t\trouter := getAuthenticatedTestRouter(authUser.ID)\n\n\t\tNewHandler(&Config{\n\t\t\tR:              router,\n\t\t\tMessageService: mockMessageService,\n\t\t\tGuildService:   mockGuildService,\n\t\t\tChannelService: mockChannelService,\n\t\t\tSocketService:  mockSocketService,\n\t\t})\n\n\t\t// a response recorder for getting written http response\n\t\trr := httptest.NewRecorder()\n\n\t\t// use bytes.NewBuffer to create a reader\n\t\trequest, err := http.NewRequest(http.MethodDelete, \"/api/messages/\"+mockMessage.ID, nil)\n\t\tassert.NoError(t, err)\n\n\t\trequest.Header.Set(\"Content-Type\", \"application/json\")\n\n\t\trespBody, _ := json.Marshal(gin.H{\n\t\t\t\"error\": mockError,\n\t\t})\n\t\tassert.NoError(t, err)\n\n\t\trouter.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusInternalServerError, rr.Code)\n\t\tassert.Equal(t, respBody, rr.Body.Bytes())\n\n\t\tmockMessageService.AssertExpectations(t)\n\t\tmockChannelService.AssertExpectations(t)\n\t\tmockGuildService.AssertExpectations(t)\n\t\tmockSocketService.AssertNotCalled(t, \"EmitDeleteMessage\")\n\t})\n}\n"
  },
  {
    "path": "server/handler/middleware/auth_user.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n)\n\n// AuthUser checks if the request contains a valid session\n// and saves the session's userId in the context\nfunc AuthUser() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tsession := sessions.Default(c)\n\t\tid := session.Get(\"userId\")\n\n\t\tif id == nil {\n\t\t\te := apperrors.NewAuthorization(apperrors.InvalidSession)\n\t\t\tc.JSON(e.Status(), gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\tc.Abort()\n\t\t\treturn\n\t\t}\n\n\t\tuserId := id.(string)\n\n\t\tc.Set(\"userId\", userId)\n\n\t\t// Recreate session to extend its lifetime\n\t\tsession.Set(\"userId\", id)\n\t\tif err := session.Save(); err != nil {\n\t\t\tlog.Printf(\"Failed recreate the session: %v\\n\", err.Error())\n\t\t}\n\n\t\tc.Next()\n\t}\n}\n"
  },
  {
    "path": "server/handler/middleware/auth_user_test.go",
    "content": "package middleware\n\nimport (\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-contrib/sessions/cookie\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/service\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAuthUser(t *testing.T) {\n\tgin.SetMode(gin.TestMode)\n\n\tuid := service.GenerateId()\n\n\tt.Run(\"Adds an userId to context\", func(t *testing.T) {\n\t\trr := httptest.NewRecorder()\n\n\t\t_, r := gin.CreateTestContext(rr)\n\t\tstore := cookie.NewStore([]byte(\"secret\"))\n\t\tr.Use(sessions.Sessions(model.CookieName, store))\n\n\t\tr.Use(func(c *gin.Context) {\n\t\t\tsession := sessions.Default(c)\n\t\t\tsession.Set(\"userId\", uid)\n\t\t})\n\n\t\tvar contextUserId string\n\n\t\tr.GET(\"/api/accounts\", AuthUser(), func(c *gin.Context) {\n\t\t\tcontextKeyVal, _ := c.Get(\"userId\")\n\t\t\tcontextUserId = contextKeyVal.(string)\n\t\t})\n\n\t\trequest, _ := http.NewRequest(http.MethodGet, \"/api/accounts\", http.NoBody)\n\t\tr.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusOK, rr.Code)\n\t\tassert.Equal(t, contextUserId, uid)\n\t})\n\n\tt.Run(\"Missing Session\", func(t *testing.T) {\n\t\trr := httptest.NewRecorder()\n\n\t\t// creates a test context and gin engine\n\t\t_, r := gin.CreateTestContext(rr)\n\t\tstore := cookie.NewStore([]byte(\"secret\"))\n\t\tr.Use(sessions.Sessions(model.CookieName, store))\n\n\t\tr.GET(\"/api/accounts\", AuthUser())\n\n\t\trequest, _ := http.NewRequest(http.MethodGet, \"/api/accounts\", http.NoBody)\n\n\t\tr.ServeHTTP(rr, request)\n\n\t\tassert.Equal(t, http.StatusUnauthorized, rr.Code)\n\t})\n}\n"
  },
  {
    "path": "server/handler/middleware/timeout.go",
    "content": "package middleware\n\n/*\n * Inspired by Golang's TimeoutHandler: https://golang.org/src/net/http/server.go?s=101514:101582#L3212\n * and gin-timeout: https://github.com/vearne/gin-timeout\n */\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// Timeout wraps the request context with a timeout\nfunc Timeout(timeout time.Duration, errTimeout *apperrors.Error) gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\t// set Gin's writer as our custom writer\n\t\ttw := &timeoutWriter{ResponseWriter: c.Writer, h: make(http.Header)}\n\t\tc.Writer = tw\n\n\t\t// wrap the request context with a timeout\n\t\tctx, cancel := context.WithTimeout(c.Request.Context(), timeout)\n\t\tdefer cancel()\n\n\t\t// update gin request context\n\t\tc.Request = c.Request.WithContext(ctx)\n\n\t\tfinished := make(chan struct{}) // to indicate handler finished\n\t\tpanicChan := make(chan any, 1)  // used to handle panics if we can't recover\n\n\t\tgo func() {\n\t\t\tdefer func() {\n\t\t\t\tif p := recover(); p != nil {\n\t\t\t\t\tpanicChan <- p\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tc.Next() // calls subsequent middleware(s) and handler\n\t\t\tfinished <- struct{}{}\n\t\t}()\n\n\t\tselect {\n\t\tcase <-panicChan:\n\t\t\t// if we cannot recover from panic,\n\t\t\t// send internal server error\n\t\t\te := apperrors.NewInternal()\n\t\t\ttw.ResponseWriter.WriteHeader(e.Status())\n\t\t\teResp, _ := json.Marshal(gin.H{\n\t\t\t\t\"error\": e,\n\t\t\t})\n\t\t\t_, _ = tw.ResponseWriter.Write(eResp)\n\t\tcase <-finished:\n\t\t\t// if finished, set headers and write resp\n\t\t\ttw.mu.Lock()\n\t\t\tdefer tw.mu.Unlock()\n\t\t\t// map Headers from tw.Header() (written to by gin)\n\t\t\t// to tw.ResponseWriter for response\n\t\t\tdst := tw.ResponseWriter.Header()\n\t\t\tfor k, vv := range tw.Header() {\n\t\t\t\tdst[k] = vv\n\t\t\t}\n\t\t\ttw.ResponseWriter.WriteHeader(tw.code)\n\t\t\t// tw.wbuf will have been written to already when gin writes to tw.Write()\n\t\t\t_, _ = tw.ResponseWriter.Write(tw.wbuf.Bytes())\n\t\tcase <-ctx.Done():\n\t\t\t// timeout has occurred, send errTimeout and write headers\n\t\t\ttw.mu.Lock()\n\t\t\tdefer tw.mu.Unlock()\n\t\t\t// ResponseWriter from gin\n\t\t\ttw.ResponseWriter.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\ttw.ResponseWriter.WriteHeader(errTimeout.Status())\n\t\t\teResp, _ := json.Marshal(gin.H{\n\t\t\t\t\"error\": errTimeout,\n\t\t\t})\n\t\t\t_, _ = tw.ResponseWriter.Write(eResp)\n\t\t\tc.Abort()\n\t\t\ttw.SetTimedOut()\n\t\t}\n\t}\n}\n\n// implements http.Writer, but tracks if Writer has timed out\n// or has already written its header to prevent\n// header and body overwrites\n// also locks access to this writer to prevent race conditions\n// holds the gin.ResponseWriter which we'll manually call Write()\n// on in the middleware function to send response\ntype timeoutWriter struct {\n\tgin.ResponseWriter\n\th    http.Header\n\twbuf bytes.Buffer // The zero value for Buffer is an empty buffer ready to use.\n\n\tmu          sync.Mutex\n\ttimedOut    bool\n\twroteHeader bool\n\tcode        int\n}\n\n// Writes the response, but first makes sure there\n// hasn't already been a timeout\n// In http.ResponseWriter interface\nfunc (tw *timeoutWriter) Write(b []byte) (int, error) {\n\ttw.mu.Lock()\n\tdefer tw.mu.Unlock()\n\tif tw.timedOut {\n\t\treturn 0, nil\n\t}\n\n\treturn tw.wbuf.Write(b)\n}\n\n// WriteHeader In http.ResponseWriter interface\nfunc (tw *timeoutWriter) WriteHeader(code int) {\n\tcheckWriteHeaderCode(code)\n\ttw.mu.Lock()\n\tdefer tw.mu.Unlock()\n\t// We do not write the header if we've timed out or written the header\n\tif tw.timedOut || tw.wroteHeader {\n\t\treturn\n\t}\n\ttw.writeHeader(code)\n}\n\n// set that the header has been written\nfunc (tw *timeoutWriter) writeHeader(code int) {\n\ttw.wroteHeader = true\n\ttw.code = code\n}\n\n// Header \"relays\" the header, h, set in struct\n// In http.ResponseWriter interface\nfunc (tw *timeoutWriter) Header() http.Header {\n\treturn tw.h\n}\n\n// SetTimedOut sets timedOut field to true\nfunc (tw *timeoutWriter) SetTimedOut() {\n\ttw.timedOut = true\n}\n\nfunc checkWriteHeaderCode(code int) {\n\tif code < 100 || code > 999 {\n\t\tpanic(fmt.Sprintf(\"invalid WriteHeader code %v\", code))\n\t}\n}\n"
  },
  {
    "path": "server/handler/mime_type.go",
    "content": "package handler\n\nvar validImageTypes = map[string]bool{\n\t\"image/jpeg\": true,\n\t\"image/png\":  true,\n}\n\n// IsAllowedImageType determines if image is among types defined\n// in map of allowed images\nfunc isAllowedImageType(mimeType string) bool {\n\t_, exists := validImageTypes[mimeType]\n\n\treturn exists\n}\n\nvar validFileTypes = map[string]bool{\n\t\"image/jpeg\": true,\n\t\"image/png\":  true,\n\t\"audio/mp3\":  true,\n\t\"audio/wave\": true,\n}\n\n// isAllowedFileType determines if the file is among types defined\n// in map of allowed file types\nfunc isAllowedFileType(mimeType string) bool {\n\t_, exists := validFileTypes[mimeType]\n\n\treturn exists\n}\n"
  },
  {
    "path": "server/handler/test_helpers.go",
    "content": "package handler\n\nimport (\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-contrib/sessions/cookie\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/model\"\n)\n\nfunc getAuthenticatedTestRouter(uid string) *gin.Engine {\n\trouter := gin.Default()\n\tstore := cookie.NewStore([]byte(\"secret\"))\n\trouter.Use(sessions.Sessions(model.CookieName, store))\n\n\trouter.Use(func(c *gin.Context) {\n\t\tsession := sessions.Default(c)\n\t\tsession.Set(\"userId\", uid)\n\t\tc.Set(\"userId\", uid)\n\t})\n\n\treturn router\n}\n\nfunc getTestRouter() *gin.Engine {\n\trouter := gin.Default()\n\tstore := cookie.NewStore([]byte(\"secret\"))\n\trouter.Use(sessions.Sessions(model.CookieName, store))\n\treturn router\n}\n\nfunc getTestFieldErrorResponse(field, message string) gin.H {\n\treturn gin.H{\n\t\t\"errors\": []model.FieldError{\n\t\t\t{\n\t\t\t\tField:   field,\n\t\t\t\tMessage: message,\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "server/injection.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"github.com/gin-contrib/sessions\"\n\t\"github.com/gin-contrib/sessions/redis\"\n\t\"github.com/gin-gonic/gin\"\n\tcors \"github.com/rs/cors/wrapper/gin\"\n\t\"github.com/sentrionic/valkyrie/config\"\n\t\"github.com/sentrionic/valkyrie/handler\"\n\t\"github.com/sentrionic/valkyrie/handler/middleware\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/repository\"\n\t\"github.com/sentrionic/valkyrie/service\"\n\t\"github.com/sentrionic/valkyrie/ws\"\n\t\"github.com/ulule/limiter/v3\"\n\tmgin \"github.com/ulule/limiter/v3/drivers/middleware/gin\"\n\tsredis \"github.com/ulule/limiter/v3/drivers/store/redis\"\n\t\"log\"\n\t\"net/http\"\n\t\"time\"\n)\n\nfunc inject(d *dataSources, cfg config.Config) (*gin.Engine, error) {\n\tlog.Println(\"Injecting data sources\")\n\n\t// Repository layer\n\tuserRepository := repository.NewUserRepository(d.DB)\n\tfriendRepository := repository.NewFriendRepository(d.DB)\n\tguildRepository := repository.NewGuildRepository(d.DB)\n\tchannelRepository := repository.NewChannelRepository(d.DB)\n\tmessageRepository := repository.NewMessageRepository(d.DB)\n\n\tfileRepository := repository.NewFileRepository(d.S3Session, cfg.BucketName)\n\tredisRepository := repository.NewRedisRepository(d.RedisClient)\n\n\tmailRepository := repository.NewMailRepository(cfg.GmailUser, cfg.GmailPassword, cfg.CorsOrigin)\n\n\t// Service Layer\n\tuserService := service.NewUserService(&service.USConfig{\n\t\tUserRepository:  userRepository,\n\t\tFileRepository:  fileRepository,\n\t\tRedisRepository: redisRepository,\n\t\tMailRepository:  mailRepository,\n\t})\n\n\tfriendService := service.NewFriendService(&service.FSConfig{\n\t\tUserRepository:   userRepository,\n\t\tFriendRepository: friendRepository,\n\t})\n\n\tguildService := service.NewGuildService(&service.GSConfig{\n\t\tUserRepository:    userRepository,\n\t\tFileRepository:    fileRepository,\n\t\tRedisRepository:   redisRepository,\n\t\tGuildRepository:   guildRepository,\n\t\tChannelRepository: channelRepository,\n\t})\n\n\tchannelService := service.NewChannelService(&service.CSConfig{\n\t\tChannelRepository: channelRepository,\n\t\tGuildRepository:   guildRepository,\n\t})\n\n\tmessageService := service.NewMessageService(&service.MSConfig{\n\t\tMessageRepository: messageRepository,\n\t\tFileRepository:    fileRepository,\n\t})\n\n\t// initialize gin.Engine\n\trouter := gin.Default()\n\n\t// set cors settings\n\tc := cors.New(cors.Options{\n\t\tAllowedOrigins:   []string{cfg.CorsOrigin},\n\t\tAllowCredentials: true,\n\t\tAllowedMethods:   []string{\"GET\", \"POST\", \"PUT\", \"DELETE\"},\n\t})\n\trouter.Use(c)\n\n\tredisURL := d.RedisClient.Options().Addr\n\tpassword := d.RedisClient.Options().Password\n\n\t// initialize session store\n\tstore, err := redis.NewStore(10, \"tcp\", redisURL, password, []byte(cfg.SessionSecret))\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"could not initialize redis session store: %w\", err)\n\t}\n\n\tstore.Options(sessions.Options{\n\t\tDomain:   cfg.Domain,\n\t\tMaxAge:   60 * 60 * 24 * 7, // 7 days\n\t\tSecure:   gin.Mode() == gin.ReleaseMode,\n\t\tHttpOnly: true,\n\t\tPath:     \"/\",\n\t\tSameSite: http.SameSiteLaxMode,\n\t})\n\n\trouter.Use(sessions.Sessions(model.CookieName, store))\n\n\t// add rate limit\n\trate := limiter.Rate{\n\t\tPeriod: 1 * time.Hour,\n\t\tLimit:  1500,\n\t}\n\n\tlimitStore, _ := sredis.NewStore(d.RedisClient)\n\n\trateLimiter := mgin.NewMiddleware(limiter.New(limitStore, rate))\n\trouter.Use(rateLimiter)\n\n\t// Websockets Setup\n\thub := ws.NewWebsocketHub(&ws.Config{\n\t\tUserService:    userService,\n\t\tGuildService:   guildService,\n\t\tChannelService: channelService,\n\t\tRedis:          d.RedisClient,\n\t})\n\tgo hub.Run()\n\n\trouter.GET(\"/ws\", middleware.AuthUser(), func(c *gin.Context) {\n\t\tws.ServeWs(hub, c)\n\t})\n\n\tsocketService := service.NewSocketService(&service.SSConfig{\n\t\tHub:               *hub,\n\t\tGuildRepository:   guildRepository,\n\t\tChannelRepository: channelRepository,\n\t})\n\n\thandler.NewHandler(&handler.Config{\n\t\tR:               router,\n\t\tUserService:     userService,\n\t\tFriendService:   friendService,\n\t\tGuildService:    guildService,\n\t\tChannelService:  channelService,\n\t\tMessageService:  messageService,\n\t\tSocketService:   socketService,\n\t\tTimeoutDuration: time.Duration(cfg.HandlerTimeOut) * time.Second,\n\t\tMaxBodyBytes:    cfg.MaxBodyBytes,\n\t})\n\n\treturn router, nil\n}\n"
  },
  {
    "path": "server/main.go",
    "content": "package main\n\nimport (\n\t\"context\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/sentrionic/valkyrie/config\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/joho/godotenv\"\n)\n\n// @title Valkyrie API\n// @version 1.0\n// @description Valkyrie REST API Specs. This service uses sessions for authentication\n\n// @license.name Apache 2.0\n// @host localhost:<PORT>\n// @BasePath /api\n\nfunc main() {\n\tlog.Println(\"Starting server...\")\n\n\t// Load dev env from .env file\n\tif gin.Mode() != gin.ReleaseMode {\n\t\terr := godotenv.Load()\n\t\tif err != nil {\n\t\t\tlog.Fatalln(\"Error loading .env file\")\n\t\t}\n\t}\n\n\tctx := context.Background()\n\tcfg, err := config.LoadConfig(ctx)\n\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not load the config: %v\\n\", err)\n\t}\n\n\t// initialize data sources\n\tds, err := initDS(ctx, cfg)\n\n\tif err != nil {\n\t\tlog.Fatalf(\"Unable to initialize data sources: %v\\n\", err)\n\t}\n\n\trouter, err := inject(ds, cfg)\n\n\tif err != nil {\n\t\tlog.Fatalf(\"Failure to inject data sources: %v\\n\", err)\n\t}\n\n\tsrv := &http.Server{\n\t\tAddr:    \":\" + cfg.Port,\n\t\tHandler: router,\n\t}\n\n\t// Graceful server shutdown - https://github.com/gin-gonic/examples/blob/master/graceful-shutdown/graceful-shutdown/server.go\n\tgo func() {\n\t\tif err = srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {\n\t\t\tlog.Fatalf(\"Failed to initialize server: %v\\n\", err)\n\t\t}\n\t}()\n\n\tlog.Printf(\"Listening on port %v\\n\", srv.Addr)\n\n\t// Wait for kill signal of channel\n\tquit := make(chan os.Signal, 1)\n\n\tsignal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)\n\n\t// This blocks until a signal is passed into the quit channel\n\t<-quit\n\n\t// The context is used to inform the server it has 5 seconds to finish\n\t// the request it is currently handling\n\tctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)\n\tdefer cancel()\n\n\t// shutdown data sources\n\tif err := ds.close(); err != nil {\n\t\tlog.Fatalf(\"A problem occurred gracefully shutting down data sources: %v\\n\", err)\n\t}\n\n\t// Shutdown server\n\tlog.Println(\"Shutting down server...\")\n\tif err = srv.Shutdown(ctx); err != nil {\n\t\tlog.Fatalf(\"Server forced to shutdown: %v\\n\", err)\n\t}\n}\n"
  },
  {
    "path": "server/mocks/ChannelRepository.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// ChannelRepository is an autogenerated mock type for the ChannelRepository type\ntype ChannelRepository struct {\n\tmock.Mock\n}\n\n// AddDMChannelMembers provides a mock function with given fields: members\nfunc (_m *ChannelRepository) AddDMChannelMembers(members []model.DMMember) error {\n\tret := _m.Called(members)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]model.DMMember) error); ok {\n\t\tr0 = rf(members)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// AddPrivateChannelMembers provides a mock function with given fields: memberIds, channelId\nfunc (_m *ChannelRepository) AddPrivateChannelMembers(memberIds []string, channelId string) error {\n\tret := _m.Called(memberIds, channelId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]string, string) error); ok {\n\t\tr0 = rf(memberIds, channelId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// CleanPCMembers provides a mock function with given fields: channelId\nfunc (_m *ChannelRepository) CleanPCMembers(channelId string) error {\n\tret := _m.Called(channelId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string) error); ok {\n\t\tr0 = rf(channelId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Create provides a mock function with given fields: channel\nfunc (_m *ChannelRepository) Create(channel *model.Channel) (*model.Channel, error) {\n\tret := _m.Called(channel)\n\n\tvar r0 *model.Channel\n\tif rf, ok := ret.Get(0).(func(*model.Channel) *model.Channel); ok {\n\t\tr0 = rf(channel)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Channel)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*model.Channel) error); ok {\n\t\tr1 = rf(channel)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// DeleteChannel provides a mock function with given fields: channel\nfunc (_m *ChannelRepository) DeleteChannel(channel *model.Channel) error {\n\tret := _m.Called(channel)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Channel) error); ok {\n\t\tr0 = rf(channel)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// FindDMByUserAndChannelId provides a mock function with given fields: channelId, userId\nfunc (_m *ChannelRepository) FindDMByUserAndChannelId(channelId string, userId string) (string, error) {\n\tret := _m.Called(channelId, userId)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(string, string) string); ok {\n\t\tr0 = rf(channelId, userId)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(channelId, userId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// Get provides a mock function with given fields: userId, guildId\nfunc (_m *ChannelRepository) Get(userId string, guildId string) (*[]model.ChannelResponse, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *[]model.ChannelResponse\n\tif rf, ok := ret.Get(0).(func(string, string) *[]model.ChannelResponse); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.ChannelResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetById provides a mock function with given fields: channelId\nfunc (_m *ChannelRepository) GetById(channelId string) (*model.Channel, error) {\n\tret := _m.Called(channelId)\n\n\tvar r0 *model.Channel\n\tif rf, ok := ret.Get(0).(func(string) *model.Channel); ok {\n\t\tr0 = rf(channelId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Channel)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(channelId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetDMMemberIds provides a mock function with given fields: channelId\nfunc (_m *ChannelRepository) GetDMMemberIds(channelId string) (*[]string, error) {\n\tret := _m.Called(channelId)\n\n\tvar r0 *[]string\n\tif rf, ok := ret.Get(0).(func(string) *[]string); ok {\n\t\tr0 = rf(channelId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]string)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(channelId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetDirectMessageChannel provides a mock function with given fields: userId, memberId\nfunc (_m *ChannelRepository) GetDirectMessageChannel(userId string, memberId string) (*string, error) {\n\tret := _m.Called(userId, memberId)\n\n\tvar r0 *string\n\tif rf, ok := ret.Get(0).(func(string, string) *string); ok {\n\t\tr0 = rf(userId, memberId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*string)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, memberId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetDirectMessages provides a mock function with given fields: userId\nfunc (_m *ChannelRepository) GetDirectMessages(userId string) (*[]model.DirectMessage, error) {\n\tret := _m.Called(userId)\n\n\tvar r0 *[]model.DirectMessage\n\tif rf, ok := ret.Get(0).(func(string) *[]model.DirectMessage); ok {\n\t\tr0 = rf(userId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.DirectMessage)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(userId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetGuildDefault provides a mock function with given fields: guildId\nfunc (_m *ChannelRepository) GetGuildDefault(guildId string) (*model.Channel, error) {\n\tret := _m.Called(guildId)\n\n\tvar r0 *model.Channel\n\tif rf, ok := ret.Get(0).(func(string) *model.Channel); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Channel)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetPrivateChannelMembers provides a mock function with given fields: channelId\nfunc (_m *ChannelRepository) GetPrivateChannelMembers(channelId string) (*[]string, error) {\n\tret := _m.Called(channelId)\n\n\tvar r0 *[]string\n\tif rf, ok := ret.Get(0).(func(string) *[]string); ok {\n\t\tr0 = rf(channelId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]string)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(channelId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// OpenDMForAll provides a mock function with given fields: dmId\nfunc (_m *ChannelRepository) OpenDMForAll(dmId string) error {\n\tret := _m.Called(dmId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string) error); ok {\n\t\tr0 = rf(dmId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// RemovePrivateChannelMembers provides a mock function with given fields: memberIds, channelId\nfunc (_m *ChannelRepository) RemovePrivateChannelMembers(memberIds []string, channelId string) error {\n\tret := _m.Called(memberIds, channelId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]string, string) error); ok {\n\t\tr0 = rf(memberIds, channelId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// SetDirectMessageStatus provides a mock function with given fields: dmId, userId, isOpen\nfunc (_m *ChannelRepository) SetDirectMessageStatus(dmId string, userId string, isOpen bool) error {\n\tret := _m.Called(dmId, userId, isOpen)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string, bool) error); ok {\n\t\tr0 = rf(dmId, userId, isOpen)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateChannel provides a mock function with given fields: channel\nfunc (_m *ChannelRepository) UpdateChannel(channel *model.Channel) error {\n\tret := _m.Called(channel)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Channel) error); ok {\n\t\tr0 = rf(channel)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewChannelRepository creates a new instance of ChannelRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewChannelRepository(t testing.TB) *ChannelRepository {\n\tmock := &ChannelRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/ChannelService.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// ChannelService is an autogenerated mock type for the ChannelService type\ntype ChannelService struct {\n\tmock.Mock\n}\n\n// AddDMChannelMembers provides a mock function with given fields: memberIds, channelId, userId\nfunc (_m *ChannelService) AddDMChannelMembers(memberIds []string, channelId string, userId string) error {\n\tret := _m.Called(memberIds, channelId, userId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]string, string, string) error); ok {\n\t\tr0 = rf(memberIds, channelId, userId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// AddPrivateChannelMembers provides a mock function with given fields: memberIds, channelId\nfunc (_m *ChannelService) AddPrivateChannelMembers(memberIds []string, channelId string) error {\n\tret := _m.Called(memberIds, channelId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]string, string) error); ok {\n\t\tr0 = rf(memberIds, channelId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// CleanPCMembers provides a mock function with given fields: channelId\nfunc (_m *ChannelService) CleanPCMembers(channelId string) error {\n\tret := _m.Called(channelId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string) error); ok {\n\t\tr0 = rf(channelId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// CreateChannel provides a mock function with given fields: channel\nfunc (_m *ChannelService) CreateChannel(channel *model.Channel) (*model.Channel, error) {\n\tret := _m.Called(channel)\n\n\tvar r0 *model.Channel\n\tif rf, ok := ret.Get(0).(func(*model.Channel) *model.Channel); ok {\n\t\tr0 = rf(channel)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Channel)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*model.Channel) error); ok {\n\t\tr1 = rf(channel)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// DeleteChannel provides a mock function with given fields: channel\nfunc (_m *ChannelService) DeleteChannel(channel *model.Channel) error {\n\tret := _m.Called(channel)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Channel) error); ok {\n\t\tr0 = rf(channel)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Get provides a mock function with given fields: channelId\nfunc (_m *ChannelService) Get(channelId string) (*model.Channel, error) {\n\tret := _m.Called(channelId)\n\n\tvar r0 *model.Channel\n\tif rf, ok := ret.Get(0).(func(string) *model.Channel); ok {\n\t\tr0 = rf(channelId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Channel)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(channelId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetChannels provides a mock function with given fields: userId, guildId\nfunc (_m *ChannelService) GetChannels(userId string, guildId string) (*[]model.ChannelResponse, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *[]model.ChannelResponse\n\tif rf, ok := ret.Get(0).(func(string, string) *[]model.ChannelResponse); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.ChannelResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetDMByUserAndChannel provides a mock function with given fields: userId, channelId\nfunc (_m *ChannelService) GetDMByUserAndChannel(userId string, channelId string) (string, error) {\n\tret := _m.Called(userId, channelId)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(string, string) string); ok {\n\t\tr0 = rf(userId, channelId)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, channelId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetDirectMessageChannel provides a mock function with given fields: userId, memberId\nfunc (_m *ChannelService) GetDirectMessageChannel(userId string, memberId string) (*string, error) {\n\tret := _m.Called(userId, memberId)\n\n\tvar r0 *string\n\tif rf, ok := ret.Get(0).(func(string, string) *string); ok {\n\t\tr0 = rf(userId, memberId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*string)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, memberId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetDirectMessages provides a mock function with given fields: userId\nfunc (_m *ChannelService) GetDirectMessages(userId string) (*[]model.DirectMessage, error) {\n\tret := _m.Called(userId)\n\n\tvar r0 *[]model.DirectMessage\n\tif rf, ok := ret.Get(0).(func(string) *[]model.DirectMessage); ok {\n\t\tr0 = rf(userId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.DirectMessage)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(userId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetPrivateChannelMembers provides a mock function with given fields: channelId\nfunc (_m *ChannelService) GetPrivateChannelMembers(channelId string) (*[]string, error) {\n\tret := _m.Called(channelId)\n\n\tvar r0 *[]string\n\tif rf, ok := ret.Get(0).(func(string) *[]string); ok {\n\t\tr0 = rf(channelId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]string)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(channelId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// IsChannelMember provides a mock function with given fields: channel, userId\nfunc (_m *ChannelService) IsChannelMember(channel *model.Channel, userId string) error {\n\tret := _m.Called(channel, userId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Channel, string) error); ok {\n\t\tr0 = rf(channel, userId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// OpenDMForAll provides a mock function with given fields: dmId\nfunc (_m *ChannelService) OpenDMForAll(dmId string) error {\n\tret := _m.Called(dmId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string) error); ok {\n\t\tr0 = rf(dmId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// RemovePrivateChannelMembers provides a mock function with given fields: memberIds, channelId\nfunc (_m *ChannelService) RemovePrivateChannelMembers(memberIds []string, channelId string) error {\n\tret := _m.Called(memberIds, channelId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func([]string, string) error); ok {\n\t\tr0 = rf(memberIds, channelId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// SetDirectMessageStatus provides a mock function with given fields: dmId, userId, isOpen\nfunc (_m *ChannelService) SetDirectMessageStatus(dmId string, userId string, isOpen bool) error {\n\tret := _m.Called(dmId, userId, isOpen)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string, bool) error); ok {\n\t\tr0 = rf(dmId, userId, isOpen)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateChannel provides a mock function with given fields: channel\nfunc (_m *ChannelService) UpdateChannel(channel *model.Channel) error {\n\tret := _m.Called(channel)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Channel) error); ok {\n\t\tr0 = rf(channel)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewChannelService creates a new instance of ChannelService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewChannelService(t testing.TB) *ChannelService {\n\tmock := &ChannelService{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/FileRepository.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmock \"github.com/stretchr/testify/mock\"\n\n\tmultipart \"mime/multipart\"\n\n\ttesting \"testing\"\n)\n\n// FileRepository is an autogenerated mock type for the FileRepository type\ntype FileRepository struct {\n\tmock.Mock\n}\n\n// DeleteImage provides a mock function with given fields: key\nfunc (_m *FileRepository) DeleteImage(key string) error {\n\tret := _m.Called(key)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string) error); ok {\n\t\tr0 = rf(key)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UploadAvatar provides a mock function with given fields: header, directory\nfunc (_m *FileRepository) UploadAvatar(header *multipart.FileHeader, directory string) (string, error) {\n\tret := _m.Called(header, directory)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(*multipart.FileHeader, string) string); ok {\n\t\tr0 = rf(header, directory)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*multipart.FileHeader, string) error); ok {\n\t\tr1 = rf(header, directory)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// UploadFile provides a mock function with given fields: header, directory, filename, mimetype\nfunc (_m *FileRepository) UploadFile(header *multipart.FileHeader, directory string, filename string, mimetype string) (string, error) {\n\tret := _m.Called(header, directory, filename, mimetype)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(*multipart.FileHeader, string, string, string) string); ok {\n\t\tr0 = rf(header, directory, filename, mimetype)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*multipart.FileHeader, string, string, string) error); ok {\n\t\tr1 = rf(header, directory, filename, mimetype)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// NewFileRepository creates a new instance of FileRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewFileRepository(t testing.TB) *FileRepository {\n\tmock := &FileRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/FriendRepository.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// FriendRepository is an autogenerated mock type for the FriendRepository type\ntype FriendRepository struct {\n\tmock.Mock\n}\n\n// DeleteRequest provides a mock function with given fields: memberId, userId\nfunc (_m *FriendRepository) DeleteRequest(memberId string, userId string) error {\n\tret := _m.Called(memberId, userId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(memberId, userId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// FindByID provides a mock function with given fields: id\nfunc (_m *FriendRepository) FindByID(id string) (*model.User, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string) *model.User); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// FriendsList provides a mock function with given fields: id\nfunc (_m *FriendRepository) FriendsList(id string) (*[]model.Friend, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *[]model.Friend\n\tif rf, ok := ret.Get(0).(func(string) *[]model.Friend); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.Friend)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// RemoveFriend provides a mock function with given fields: memberId, userId\nfunc (_m *FriendRepository) RemoveFriend(memberId string, userId string) error {\n\tret := _m.Called(memberId, userId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(memberId, userId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// RequestList provides a mock function with given fields: id\nfunc (_m *FriendRepository) RequestList(id string) (*[]model.FriendRequest, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *[]model.FriendRequest\n\tif rf, ok := ret.Get(0).(func(string) *[]model.FriendRequest); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.FriendRequest)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// Save provides a mock function with given fields: user\nfunc (_m *FriendRepository) Save(user *model.User) error {\n\tret := _m.Called(user)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.User) error); ok {\n\t\tr0 = rf(user)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewFriendRepository creates a new instance of FriendRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewFriendRepository(t testing.TB) *FriendRepository {\n\tmock := &FriendRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/FriendService.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// FriendService is an autogenerated mock type for the FriendService type\ntype FriendService struct {\n\tmock.Mock\n}\n\n// DeleteRequest provides a mock function with given fields: memberId, userId\nfunc (_m *FriendService) DeleteRequest(memberId string, userId string) error {\n\tret := _m.Called(memberId, userId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(memberId, userId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// GetFriends provides a mock function with given fields: id\nfunc (_m *FriendService) GetFriends(id string) (*[]model.Friend, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *[]model.Friend\n\tif rf, ok := ret.Get(0).(func(string) *[]model.Friend); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.Friend)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetMemberById provides a mock function with given fields: id\nfunc (_m *FriendService) GetMemberById(id string) (*model.User, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string) *model.User); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetRequests provides a mock function with given fields: id\nfunc (_m *FriendService) GetRequests(id string) (*[]model.FriendRequest, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *[]model.FriendRequest\n\tif rf, ok := ret.Get(0).(func(string) *[]model.FriendRequest); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.FriendRequest)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// RemoveFriend provides a mock function with given fields: memberId, userId\nfunc (_m *FriendService) RemoveFriend(memberId string, userId string) error {\n\tret := _m.Called(memberId, userId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(memberId, userId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// SaveRequests provides a mock function with given fields: user\nfunc (_m *FriendService) SaveRequests(user *model.User) error {\n\tret := _m.Called(user)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.User) error); ok {\n\t\tr0 = rf(user)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewFriendService creates a new instance of FriendService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewFriendService(t testing.TB) *FriendService {\n\tmock := &FriendService{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/GuildRepository.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// GuildRepository is an autogenerated mock type for the GuildRepository type\ntype GuildRepository struct {\n\tmock.Mock\n}\n\n// Create provides a mock function with given fields: guild\nfunc (_m *GuildRepository) Create(guild *model.Guild) (*model.Guild, error) {\n\tret := _m.Called(guild)\n\n\tvar r0 *model.Guild\n\tif rf, ok := ret.Get(0).(func(*model.Guild) *model.Guild); ok {\n\t\tr0 = rf(guild)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Guild)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*model.Guild) error); ok {\n\t\tr1 = rf(guild)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// Delete provides a mock function with given fields: guildId\nfunc (_m *GuildRepository) Delete(guildId string) error {\n\tret := _m.Called(guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string) error); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// FindByID provides a mock function with given fields: id\nfunc (_m *GuildRepository) FindByID(id string) (*model.Guild, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *model.Guild\n\tif rf, ok := ret.Get(0).(func(string) *model.Guild); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Guild)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// FindUserByID provides a mock function with given fields: uid\nfunc (_m *GuildRepository) FindUserByID(uid string) (*model.User, error) {\n\tret := _m.Called(uid)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string) *model.User); ok {\n\t\tr0 = rf(uid)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(uid)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// FindUsersByIds provides a mock function with given fields: ids, guildId\nfunc (_m *GuildRepository) FindUsersByIds(ids []string, guildId string) (*[]model.User, error) {\n\tret := _m.Called(ids, guildId)\n\n\tvar r0 *[]model.User\n\tif rf, ok := ret.Get(0).(func([]string, string) *[]model.User); ok {\n\t\tr0 = rf(ids, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func([]string, string) error); ok {\n\t\tr1 = rf(ids, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetBanList provides a mock function with given fields: guildId\nfunc (_m *GuildRepository) GetBanList(guildId string) (*[]model.BanResponse, error) {\n\tret := _m.Called(guildId)\n\n\tvar r0 *[]model.BanResponse\n\tif rf, ok := ret.Get(0).(func(string) *[]model.BanResponse); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.BanResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildRepository) GetMember(userId string, guildId string) (*model.User, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string, string) *model.User); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetMemberIds provides a mock function with given fields: guildId\nfunc (_m *GuildRepository) GetMemberIds(guildId string) (*[]string, error) {\n\tret := _m.Called(guildId)\n\n\tvar r0 *[]string\n\tif rf, ok := ret.Get(0).(func(string) *[]string); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]string)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetMemberSettings provides a mock function with given fields: userId, guildId\nfunc (_m *GuildRepository) GetMemberSettings(userId string, guildId string) (*model.MemberSettings, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *model.MemberSettings\n\tif rf, ok := ret.Get(0).(func(string, string) *model.MemberSettings); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.MemberSettings)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetVCMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildRepository) GetVCMember(userId string, guildId string) (*model.VCMember, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *model.VCMember\n\tif rf, ok := ret.Get(0).(func(string, string) *model.VCMember); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.VCMember)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GuildMembers provides a mock function with given fields: userId, guildId\nfunc (_m *GuildRepository) GuildMembers(userId string, guildId string) (*[]model.MemberResponse, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *[]model.MemberResponse\n\tif rf, ok := ret.Get(0).(func(string, string) *[]model.MemberResponse); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.MemberResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// List provides a mock function with given fields: uid\nfunc (_m *GuildRepository) List(uid string) (*[]model.GuildResponse, error) {\n\tret := _m.Called(uid)\n\n\tvar r0 *[]model.GuildResponse\n\tif rf, ok := ret.Get(0).(func(string) *[]model.GuildResponse); ok {\n\t\tr0 = rf(uid)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.GuildResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(uid)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// RemoveMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildRepository) RemoveMember(userId string, guildId string) error {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// RemoveVCMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildRepository) RemoveVCMember(userId string, guildId string) error {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Save provides a mock function with given fields: guild\nfunc (_m *GuildRepository) Save(guild *model.Guild) error {\n\tret := _m.Called(guild)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Guild) error); ok {\n\t\tr0 = rf(guild)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UnbanMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildRepository) UnbanMember(userId string, guildId string) error {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateMemberLastSeen provides a mock function with given fields: userId, guildId\nfunc (_m *GuildRepository) UpdateMemberLastSeen(userId string, guildId string) error {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateMemberSettings provides a mock function with given fields: settings, userId, guildId\nfunc (_m *GuildRepository) UpdateMemberSettings(settings *model.MemberSettings, userId string, guildId string) error {\n\tret := _m.Called(settings, userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.MemberSettings, string, string) error); ok {\n\t\tr0 = rf(settings, userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateVCMember provides a mock function with given fields: isMuted, isDeafened, userId, guildId\nfunc (_m *GuildRepository) UpdateVCMember(isMuted bool, isDeafened bool, userId string, guildId string) error {\n\tret := _m.Called(isMuted, isDeafened, userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(bool, bool, string, string) error); ok {\n\t\tr0 = rf(isMuted, isDeafened, userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// VCMembers provides a mock function with given fields: guildId\nfunc (_m *GuildRepository) VCMembers(guildId string) (*[]model.VCMemberResponse, error) {\n\tret := _m.Called(guildId)\n\n\tvar r0 *[]model.VCMemberResponse\n\tif rf, ok := ret.Get(0).(func(string) *[]model.VCMemberResponse); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.VCMemberResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// NewGuildRepository creates a new instance of GuildRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewGuildRepository(t testing.TB) *GuildRepository {\n\tmock := &GuildRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/GuildService.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// GuildService is an autogenerated mock type for the GuildService type\ntype GuildService struct {\n\tmock.Mock\n}\n\n// CreateGuild provides a mock function with given fields: guild\nfunc (_m *GuildService) CreateGuild(guild *model.Guild) (*model.Guild, error) {\n\tret := _m.Called(guild)\n\n\tvar r0 *model.Guild\n\tif rf, ok := ret.Get(0).(func(*model.Guild) *model.Guild); ok {\n\t\tr0 = rf(guild)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Guild)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*model.Guild) error); ok {\n\t\tr1 = rf(guild)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// DeleteGuild provides a mock function with given fields: guildId\nfunc (_m *GuildService) DeleteGuild(guildId string) error {\n\tret := _m.Called(guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string) error); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// FindUsersByIds provides a mock function with given fields: ids, guildId\nfunc (_m *GuildService) FindUsersByIds(ids []string, guildId string) (*[]model.User, error) {\n\tret := _m.Called(ids, guildId)\n\n\tvar r0 *[]model.User\n\tif rf, ok := ret.Get(0).(func([]string, string) *[]model.User); ok {\n\t\tr0 = rf(ids, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func([]string, string) error); ok {\n\t\tr1 = rf(ids, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GenerateInviteLink provides a mock function with given fields: ctx, guildId, isPermanent\nfunc (_m *GuildService) GenerateInviteLink(ctx context.Context, guildId string, isPermanent bool) (string, error) {\n\tret := _m.Called(ctx, guildId, isPermanent)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(context.Context, string, bool) string); ok {\n\t\tr0 = rf(ctx, guildId, isPermanent)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(context.Context, string, bool) error); ok {\n\t\tr1 = rf(ctx, guildId, isPermanent)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetBanList provides a mock function with given fields: guildId\nfunc (_m *GuildService) GetBanList(guildId string) (*[]model.BanResponse, error) {\n\tret := _m.Called(guildId)\n\n\tvar r0 *[]model.BanResponse\n\tif rf, ok := ret.Get(0).(func(string) *[]model.BanResponse); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.BanResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetDefaultChannel provides a mock function with given fields: guildId\nfunc (_m *GuildService) GetDefaultChannel(guildId string) (*model.Channel, error) {\n\tret := _m.Called(guildId)\n\n\tvar r0 *model.Channel\n\tif rf, ok := ret.Get(0).(func(string) *model.Channel); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Channel)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetGuild provides a mock function with given fields: id\nfunc (_m *GuildService) GetGuild(id string) (*model.Guild, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *model.Guild\n\tif rf, ok := ret.Get(0).(func(string) *model.Guild); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Guild)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetGuildIdFromInvite provides a mock function with given fields: ctx, token\nfunc (_m *GuildService) GetGuildIdFromInvite(ctx context.Context, token string) (string, error) {\n\tret := _m.Called(ctx, token)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(context.Context, string) string); ok {\n\t\tr0 = rf(ctx, token)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(context.Context, string) error); ok {\n\t\tr1 = rf(ctx, token)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetGuildMembers provides a mock function with given fields: userId, guildId\nfunc (_m *GuildService) GetGuildMembers(userId string, guildId string) (*[]model.MemberResponse, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *[]model.MemberResponse\n\tif rf, ok := ret.Get(0).(func(string, string) *[]model.MemberResponse); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.MemberResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetMemberSettings provides a mock function with given fields: userId, guildId\nfunc (_m *GuildService) GetMemberSettings(userId string, guildId string) (*model.MemberSettings, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *model.MemberSettings\n\tif rf, ok := ret.Get(0).(func(string, string) *model.MemberSettings); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.MemberSettings)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetUser provides a mock function with given fields: uid\nfunc (_m *GuildService) GetUser(uid string) (*model.User, error) {\n\tret := _m.Called(uid)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string) *model.User); ok {\n\t\tr0 = rf(uid)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(uid)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetUserGuilds provides a mock function with given fields: uid\nfunc (_m *GuildService) GetUserGuilds(uid string) (*[]model.GuildResponse, error) {\n\tret := _m.Called(uid)\n\n\tvar r0 *[]model.GuildResponse\n\tif rf, ok := ret.Get(0).(func(string) *[]model.GuildResponse); ok {\n\t\tr0 = rf(uid)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.GuildResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(uid)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetVCMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildService) GetVCMember(userId string, guildId string) (*model.VCMember, error) {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 *model.VCMember\n\tif rf, ok := ret.Get(0).(func(string, string) *model.VCMember); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.VCMember)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(userId, guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetVCMembers provides a mock function with given fields: guildId\nfunc (_m *GuildService) GetVCMembers(guildId string) (*[]model.VCMemberResponse, error) {\n\tret := _m.Called(guildId)\n\n\tvar r0 *[]model.VCMemberResponse\n\tif rf, ok := ret.Get(0).(func(string) *[]model.VCMemberResponse); ok {\n\t\tr0 = rf(guildId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.VCMemberResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(guildId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// InvalidateInvites provides a mock function with given fields: ctx, guild\nfunc (_m *GuildService) InvalidateInvites(ctx context.Context, guild *model.Guild) {\n\t_m.Called(ctx, guild)\n}\n\n// RemoveMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildService) RemoveMember(userId string, guildId string) error {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// RemoveVCMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildService) RemoveVCMember(userId string, guildId string) error {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UnbanMember provides a mock function with given fields: userId, guildId\nfunc (_m *GuildService) UnbanMember(userId string, guildId string) error {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateGuild provides a mock function with given fields: guild\nfunc (_m *GuildService) UpdateGuild(guild *model.Guild) error {\n\tret := _m.Called(guild)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Guild) error); ok {\n\t\tr0 = rf(guild)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateMemberLastSeen provides a mock function with given fields: userId, guildId\nfunc (_m *GuildService) UpdateMemberLastSeen(userId string, guildId string) error {\n\tret := _m.Called(userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateMemberSettings provides a mock function with given fields: settings, userId, guildId\nfunc (_m *GuildService) UpdateMemberSettings(settings *model.MemberSettings, userId string, guildId string) error {\n\tret := _m.Called(settings, userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.MemberSettings, string, string) error); ok {\n\t\tr0 = rf(settings, userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UpdateVCMember provides a mock function with given fields: isMuted, isDeafened, userId, guildId\nfunc (_m *GuildService) UpdateVCMember(isMuted bool, isDeafened bool, userId string, guildId string) error {\n\tret := _m.Called(isMuted, isDeafened, userId, guildId)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(bool, bool, string, string) error); ok {\n\t\tr0 = rf(isMuted, isDeafened, userId, guildId)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewGuildService creates a new instance of GuildService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewGuildService(t testing.TB) *GuildService {\n\tmock := &GuildService{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/MailRepository.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// MailRepository is an autogenerated mock type for the MailRepository type\ntype MailRepository struct {\n\tmock.Mock\n}\n\n// SendResetMail provides a mock function with given fields: email, html\nfunc (_m *MailRepository) SendResetMail(email string, html string) error {\n\tret := _m.Called(email, html)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string) error); ok {\n\t\tr0 = rf(email, html)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewMailRepository creates a new instance of MailRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewMailRepository(t testing.TB) *MailRepository {\n\tmock := &MailRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/MessageRepository.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// MessageRepository is an autogenerated mock type for the MessageRepository type\ntype MessageRepository struct {\n\tmock.Mock\n}\n\n// CreateMessage provides a mock function with given fields: params\nfunc (_m *MessageRepository) CreateMessage(params *model.Message) (*model.Message, error) {\n\tret := _m.Called(params)\n\n\tvar r0 *model.Message\n\tif rf, ok := ret.Get(0).(func(*model.Message) *model.Message); ok {\n\t\tr0 = rf(params)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Message)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*model.Message) error); ok {\n\t\tr1 = rf(params)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// DeleteMessage provides a mock function with given fields: message\nfunc (_m *MessageRepository) DeleteMessage(message *model.Message) error {\n\tret := _m.Called(message)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Message) error); ok {\n\t\tr0 = rf(message)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// GetById provides a mock function with given fields: messageId\nfunc (_m *MessageRepository) GetById(messageId string) (*model.Message, error) {\n\tret := _m.Called(messageId)\n\n\tvar r0 *model.Message\n\tif rf, ok := ret.Get(0).(func(string) *model.Message); ok {\n\t\tr0 = rf(messageId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Message)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(messageId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetMessages provides a mock function with given fields: userId, channel, cursor\nfunc (_m *MessageRepository) GetMessages(userId string, channel *model.Channel, cursor string) (*[]model.MessageResponse, error) {\n\tret := _m.Called(userId, channel, cursor)\n\n\tvar r0 *[]model.MessageResponse\n\tif rf, ok := ret.Get(0).(func(string, *model.Channel, string) *[]model.MessageResponse); ok {\n\t\tr0 = rf(userId, channel, cursor)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.MessageResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, *model.Channel, string) error); ok {\n\t\tr1 = rf(userId, channel, cursor)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// UpdateMessage provides a mock function with given fields: message\nfunc (_m *MessageRepository) UpdateMessage(message *model.Message) error {\n\tret := _m.Called(message)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Message) error); ok {\n\t\tr0 = rf(message)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewMessageRepository creates a new instance of MessageRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewMessageRepository(t testing.TB) *MessageRepository {\n\tmock := &MessageRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/MessageService.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\tmultipart \"mime/multipart\"\n\n\ttesting \"testing\"\n)\n\n// MessageService is an autogenerated mock type for the MessageService type\ntype MessageService struct {\n\tmock.Mock\n}\n\n// CreateMessage provides a mock function with given fields: params\nfunc (_m *MessageService) CreateMessage(params *model.Message) (*model.Message, error) {\n\tret := _m.Called(params)\n\n\tvar r0 *model.Message\n\tif rf, ok := ret.Get(0).(func(*model.Message) *model.Message); ok {\n\t\tr0 = rf(params)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Message)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*model.Message) error); ok {\n\t\tr1 = rf(params)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// DeleteMessage provides a mock function with given fields: message\nfunc (_m *MessageService) DeleteMessage(message *model.Message) error {\n\tret := _m.Called(message)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Message) error); ok {\n\t\tr0 = rf(message)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Get provides a mock function with given fields: messageId\nfunc (_m *MessageService) Get(messageId string) (*model.Message, error) {\n\tret := _m.Called(messageId)\n\n\tvar r0 *model.Message\n\tif rf, ok := ret.Get(0).(func(string) *model.Message); ok {\n\t\tr0 = rf(messageId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Message)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(messageId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetMessages provides a mock function with given fields: userId, channel, cursor\nfunc (_m *MessageService) GetMessages(userId string, channel *model.Channel, cursor string) (*[]model.MessageResponse, error) {\n\tret := _m.Called(userId, channel, cursor)\n\n\tvar r0 *[]model.MessageResponse\n\tif rf, ok := ret.Get(0).(func(string, *model.Channel, string) *[]model.MessageResponse); ok {\n\t\tr0 = rf(userId, channel, cursor)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]model.MessageResponse)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, *model.Channel, string) error); ok {\n\t\tr1 = rf(userId, channel, cursor)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// UpdateMessage provides a mock function with given fields: message\nfunc (_m *MessageService) UpdateMessage(message *model.Message) error {\n\tret := _m.Called(message)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.Message) error); ok {\n\t\tr0 = rf(message)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// UploadFile provides a mock function with given fields: header, channelId\nfunc (_m *MessageService) UploadFile(header *multipart.FileHeader, channelId string) (*model.Attachment, error) {\n\tret := _m.Called(header, channelId)\n\n\tvar r0 *model.Attachment\n\tif rf, ok := ret.Get(0).(func(*multipart.FileHeader, string) *model.Attachment); ok {\n\t\tr0 = rf(header, channelId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.Attachment)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*multipart.FileHeader, string) error); ok {\n\t\tr1 = rf(header, channelId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// NewMessageService creates a new instance of MessageService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewMessageService(t testing.TB) *MessageService {\n\tmock := &MessageService{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/RedisRepository.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// RedisRepository is an autogenerated mock type for the RedisRepository type\ntype RedisRepository struct {\n\tmock.Mock\n}\n\n// GetIdFromToken provides a mock function with given fields: ctx, token\nfunc (_m *RedisRepository) GetIdFromToken(ctx context.Context, token string) (string, error) {\n\tret := _m.Called(ctx, token)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(context.Context, string) string); ok {\n\t\tr0 = rf(ctx, token)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(context.Context, string) error); ok {\n\t\tr1 = rf(ctx, token)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetInvite provides a mock function with given fields: ctx, token\nfunc (_m *RedisRepository) GetInvite(ctx context.Context, token string) (string, error) {\n\tret := _m.Called(ctx, token)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(context.Context, string) string); ok {\n\t\tr0 = rf(ctx, token)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(context.Context, string) error); ok {\n\t\tr1 = rf(ctx, token)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// InvalidateInvites provides a mock function with given fields: ctx, guild\nfunc (_m *RedisRepository) InvalidateInvites(ctx context.Context, guild *model.Guild) {\n\t_m.Called(ctx, guild)\n}\n\n// SaveInvite provides a mock function with given fields: ctx, guildId, id, isPermanent\nfunc (_m *RedisRepository) SaveInvite(ctx context.Context, guildId string, id string, isPermanent bool) error {\n\tret := _m.Called(ctx, guildId, id, isPermanent)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(context.Context, string, string, bool) error); ok {\n\t\tr0 = rf(ctx, guildId, id, isPermanent)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// SetResetToken provides a mock function with given fields: ctx, id\nfunc (_m *RedisRepository) SetResetToken(ctx context.Context, id string) (string, error) {\n\tret := _m.Called(ctx, id)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(context.Context, string) string); ok {\n\t\tr0 = rf(ctx, id)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(context.Context, string) error); ok {\n\t\tr1 = rf(ctx, id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// NewRedisRepository creates a new instance of RedisRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewRedisRepository(t testing.TB) *RedisRepository {\n\tmock := &RedisRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/Request.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\ttesting \"testing\"\n\n\tmock \"github.com/stretchr/testify/mock\"\n)\n\n// Request is an autogenerated mock type for the Request type\ntype Request struct {\n\tmock.Mock\n}\n\n// validate provides a mock function with given fields:\nfunc (_m *Request) validate() error {\n\tret := _m.Called()\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func() error); ok {\n\t\tr0 = rf()\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewRequest creates a new instance of Request. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewRequest(t testing.TB) *Request {\n\tmock := &Request{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/SocketService.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// SocketService is an autogenerated mock type for the SocketService type\ntype SocketService struct {\n\tmock.Mock\n}\n\n// EmitAddFriend provides a mock function with given fields: user, member\nfunc (_m *SocketService) EmitAddFriend(user *model.User, member *model.User) {\n\t_m.Called(user, member)\n}\n\n// EmitAddFriendRequest provides a mock function with given fields: room, request\nfunc (_m *SocketService) EmitAddFriendRequest(room string, request *model.FriendRequest) {\n\t_m.Called(room, request)\n}\n\n// EmitAddMember provides a mock function with given fields: room, member\nfunc (_m *SocketService) EmitAddMember(room string, member *model.User) {\n\t_m.Called(room, member)\n}\n\n// EmitDeleteChannel provides a mock function with given fields: channel\nfunc (_m *SocketService) EmitDeleteChannel(channel *model.Channel) {\n\t_m.Called(channel)\n}\n\n// EmitDeleteGuild provides a mock function with given fields: guildId, members\nfunc (_m *SocketService) EmitDeleteGuild(guildId string, members []string) {\n\t_m.Called(guildId, members)\n}\n\n// EmitDeleteMessage provides a mock function with given fields: room, messageId\nfunc (_m *SocketService) EmitDeleteMessage(room string, messageId string) {\n\t_m.Called(room, messageId)\n}\n\n// EmitEditChannel provides a mock function with given fields: room, channel\nfunc (_m *SocketService) EmitEditChannel(room string, channel *model.ChannelResponse) {\n\t_m.Called(room, channel)\n}\n\n// EmitEditGuild provides a mock function with given fields: guild\nfunc (_m *SocketService) EmitEditGuild(guild *model.Guild) {\n\t_m.Called(guild)\n}\n\n// EmitEditMessage provides a mock function with given fields: room, message\nfunc (_m *SocketService) EmitEditMessage(room string, message *model.MessageResponse) {\n\t_m.Called(room, message)\n}\n\n// EmitNewChannel provides a mock function with given fields: room, channel\nfunc (_m *SocketService) EmitNewChannel(room string, channel *model.ChannelResponse) {\n\t_m.Called(room, channel)\n}\n\n// EmitNewDMNotification provides a mock function with given fields: channelId, user\nfunc (_m *SocketService) EmitNewDMNotification(channelId string, user *model.User) {\n\t_m.Called(channelId, user)\n}\n\n// EmitNewMessage provides a mock function with given fields: room, message\nfunc (_m *SocketService) EmitNewMessage(room string, message *model.MessageResponse) {\n\t_m.Called(room, message)\n}\n\n// EmitNewNotification provides a mock function with given fields: guildId, channelId\nfunc (_m *SocketService) EmitNewNotification(guildId string, channelId string) {\n\t_m.Called(guildId, channelId)\n}\n\n// EmitNewPrivateChannel provides a mock function with given fields: members, channel\nfunc (_m *SocketService) EmitNewPrivateChannel(members []string, channel *model.ChannelResponse) {\n\t_m.Called(members, channel)\n}\n\n// EmitRemoveFriend provides a mock function with given fields: userId, memberId\nfunc (_m *SocketService) EmitRemoveFriend(userId string, memberId string) {\n\t_m.Called(userId, memberId)\n}\n\n// EmitRemoveFromGuild provides a mock function with given fields: memberId, guildId\nfunc (_m *SocketService) EmitRemoveFromGuild(memberId string, guildId string) {\n\t_m.Called(memberId, guildId)\n}\n\n// EmitRemoveMember provides a mock function with given fields: room, memberId\nfunc (_m *SocketService) EmitRemoveMember(room string, memberId string) {\n\t_m.Called(room, memberId)\n}\n\n// EmitSendRequest provides a mock function with given fields: room\nfunc (_m *SocketService) EmitSendRequest(room string) {\n\t_m.Called(room)\n}\n\n// NewSocketService creates a new instance of SocketService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewSocketService(t testing.TB) *SocketService {\n\tmock := &SocketService{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/UserRepository.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\ttesting \"testing\"\n)\n\n// UserRepository is an autogenerated mock type for the UserRepository type\ntype UserRepository struct {\n\tmock.Mock\n}\n\n// Create provides a mock function with given fields: user\nfunc (_m *UserRepository) Create(user *model.User) (*model.User, error) {\n\tret := _m.Called(user)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(*model.User) *model.User); ok {\n\t\tr0 = rf(user)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*model.User) error); ok {\n\t\tr1 = rf(user)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// FindByEmail provides a mock function with given fields: email\nfunc (_m *UserRepository) FindByEmail(email string) (*model.User, error) {\n\tret := _m.Called(email)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string) *model.User); ok {\n\t\tr0 = rf(email)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(email)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// FindByID provides a mock function with given fields: id\nfunc (_m *UserRepository) FindByID(id string) (*model.User, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string) *model.User); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetFriendAndGuildIds provides a mock function with given fields: userId\nfunc (_m *UserRepository) GetFriendAndGuildIds(userId string) (*[]string, error) {\n\tret := _m.Called(userId)\n\n\tvar r0 *[]string\n\tif rf, ok := ret.Get(0).(func(string) *[]string); ok {\n\t\tr0 = rf(userId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]string)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(userId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetRequestCount provides a mock function with given fields: userId\nfunc (_m *UserRepository) GetRequestCount(userId string) (*int64, error) {\n\tret := _m.Called(userId)\n\n\tvar r0 *int64\n\tif rf, ok := ret.Get(0).(func(string) *int64); ok {\n\t\tr0 = rf(userId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*int64)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(userId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// Update provides a mock function with given fields: user\nfunc (_m *UserRepository) Update(user *model.User) error {\n\tret := _m.Called(user)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.User) error); ok {\n\t\tr0 = rf(user)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewUserRepository creates a new instance of UserRepository. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewUserRepository(t testing.TB) *UserRepository {\n\tmock := &UserRepository{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/mocks/UserService.go",
    "content": "// Code generated by mockery v2.12.1. DO NOT EDIT.\n\npackage mocks\n\nimport (\n\tcontext \"context\"\n\n\tmodel \"github.com/sentrionic/valkyrie/model\"\n\tmock \"github.com/stretchr/testify/mock\"\n\n\tmultipart \"mime/multipart\"\n\n\ttesting \"testing\"\n)\n\n// UserService is an autogenerated mock type for the UserService type\ntype UserService struct {\n\tmock.Mock\n}\n\n// ChangeAvatar provides a mock function with given fields: header, directory\nfunc (_m *UserService) ChangeAvatar(header *multipart.FileHeader, directory string) (string, error) {\n\tret := _m.Called(header, directory)\n\n\tvar r0 string\n\tif rf, ok := ret.Get(0).(func(*multipart.FileHeader, string) string); ok {\n\t\tr0 = rf(header, directory)\n\t} else {\n\t\tr0 = ret.Get(0).(string)\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*multipart.FileHeader, string) error); ok {\n\t\tr1 = rf(header, directory)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// ChangePassword provides a mock function with given fields: currentPassword, newPassword, user\nfunc (_m *UserService) ChangePassword(currentPassword string, newPassword string, user *model.User) error {\n\tret := _m.Called(currentPassword, newPassword, user)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string, string, *model.User) error); ok {\n\t\tr0 = rf(currentPassword, newPassword, user)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// DeleteImage provides a mock function with given fields: key\nfunc (_m *UserService) DeleteImage(key string) error {\n\tret := _m.Called(key)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(string) error); ok {\n\t\tr0 = rf(key)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// ForgotPassword provides a mock function with given fields: ctx, user\nfunc (_m *UserService) ForgotPassword(ctx context.Context, user *model.User) error {\n\tret := _m.Called(ctx, user)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(context.Context, *model.User) error); ok {\n\t\tr0 = rf(ctx, user)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// Get provides a mock function with given fields: id\nfunc (_m *UserService) Get(id string) (*model.User, error) {\n\tret := _m.Called(id)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string) *model.User); ok {\n\t\tr0 = rf(id)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(id)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetByEmail provides a mock function with given fields: email\nfunc (_m *UserService) GetByEmail(email string) (*model.User, error) {\n\tret := _m.Called(email)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string) *model.User); ok {\n\t\tr0 = rf(email)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(email)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetFriendAndGuildIds provides a mock function with given fields: userId\nfunc (_m *UserService) GetFriendAndGuildIds(userId string) (*[]string, error) {\n\tret := _m.Called(userId)\n\n\tvar r0 *[]string\n\tif rf, ok := ret.Get(0).(func(string) *[]string); ok {\n\t\tr0 = rf(userId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*[]string)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(userId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// GetRequestCount provides a mock function with given fields: userId\nfunc (_m *UserService) GetRequestCount(userId string) (*int64, error) {\n\tret := _m.Called(userId)\n\n\tvar r0 *int64\n\tif rf, ok := ret.Get(0).(func(string) *int64); ok {\n\t\tr0 = rf(userId)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*int64)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string) error); ok {\n\t\tr1 = rf(userId)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// IsEmailAlreadyInUse provides a mock function with given fields: email\nfunc (_m *UserService) IsEmailAlreadyInUse(email string) bool {\n\tret := _m.Called(email)\n\n\tvar r0 bool\n\tif rf, ok := ret.Get(0).(func(string) bool); ok {\n\t\tr0 = rf(email)\n\t} else {\n\t\tr0 = ret.Get(0).(bool)\n\t}\n\n\treturn r0\n}\n\n// Login provides a mock function with given fields: email, password\nfunc (_m *UserService) Login(email string, password string) (*model.User, error) {\n\tret := _m.Called(email, password)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(string, string) *model.User); ok {\n\t\tr0 = rf(email, password)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(string, string) error); ok {\n\t\tr1 = rf(email, password)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// Register provides a mock function with given fields: user\nfunc (_m *UserService) Register(user *model.User) (*model.User, error) {\n\tret := _m.Called(user)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(*model.User) *model.User); ok {\n\t\tr0 = rf(user)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(*model.User) error); ok {\n\t\tr1 = rf(user)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// ResetPassword provides a mock function with given fields: ctx, password, token\nfunc (_m *UserService) ResetPassword(ctx context.Context, password string, token string) (*model.User, error) {\n\tret := _m.Called(ctx, password, token)\n\n\tvar r0 *model.User\n\tif rf, ok := ret.Get(0).(func(context.Context, string, string) *model.User); ok {\n\t\tr0 = rf(ctx, password, token)\n\t} else {\n\t\tif ret.Get(0) != nil {\n\t\t\tr0 = ret.Get(0).(*model.User)\n\t\t}\n\t}\n\n\tvar r1 error\n\tif rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok {\n\t\tr1 = rf(ctx, password, token)\n\t} else {\n\t\tr1 = ret.Error(1)\n\t}\n\n\treturn r0, r1\n}\n\n// UpdateAccount provides a mock function with given fields: user\nfunc (_m *UserService) UpdateAccount(user *model.User) error {\n\tret := _m.Called(user)\n\n\tvar r0 error\n\tif rf, ok := ret.Get(0).(func(*model.User) error); ok {\n\t\tr0 = rf(user)\n\t} else {\n\t\tr0 = ret.Error(0)\n\t}\n\n\treturn r0\n}\n\n// NewUserService creates a new instance of UserService. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations.\nfunc NewUserService(t testing.TB) *UserService {\n\tmock := &UserService{}\n\tmock.Mock.Test(t)\n\n\tt.Cleanup(func() { mock.AssertExpectations(t) })\n\n\treturn mock\n}\n"
  },
  {
    "path": "server/model/app_constants.go",
    "content": "package model\n\n// Application Constants\nconst (\n\tMinimumChannels = 1\n\tMaximumChannels = 50\n\tMaximumGuilds   = 100\n\tCookieName      = \"vlk\"\n)\n"
  },
  {
    "path": "server/model/apperrors/apperrors.go",
    "content": "package apperrors\n\n// Guild Errors\nconst (\n\tNotAMember             = \"Not a member of the guild\"\n\tAlreadyMember          = \"Already a member of the guild\"\n\tGuildLimitReached      = \"The guild limit is 100\"\n\tMustBeOwner            = \"Must be the owner for that\"\n\tInvalidImageType       = \"imageFile must be 'image/jpeg' or 'image/png'\"\n\tMustBeMemberInvite     = \"Must be a member to fetch an invite\"\n\tIsPermanentError       = \"isPermanent is not a boolean\"\n\tInvalidateInvitesError = \"Only the owner can invalidate invites\"\n\tInvalidInviteError     = \"Invalid Link or the server got deleted\"\n\tBannedFromServer       = \"You are banned from this server\"\n\tDeleteGuildError       = \"Only the owner can delete their server\"\n\tOwnerCantLeave         = \"The owner cannot leave their server\"\n\tBanYourselfError       = \"You cannot ban yourself\"\n\tKickYourselfError      = \"You cannot kick yourself\"\n\tUnbanYourselfError     = \"You cannot unban yourself\"\n\tOneChannelRequired     = \"A server needs at least one channel\"\n\tChannelLimitError      = \"The channel limit is 50\"\n\tDMYourselfError        = \"You cannot dm yourself\"\n)\n\n// Account Errors\nconst (\n\tInvalidOldPassword  = \"Invalid old password\"\n\tInvalidCredentials  = \"Invalid email and password combination\"\n\tDuplicateEmail      = \"An account with that email already exists\"\n\tPasswordsDoNotMatch = \"Passwords do not match\"\n\tInvalidResetToken   = \"Invalid reset token\"\n)\n\n// Friend Errors\nconst (\n\tAddYourselfError    = \"You cannot add yourself\"\n\tRemoveYourselfError = \"You cannot remove yourself\"\n\tAcceptYourselfError = \"You cannot accept yourself\"\n\tCancelYourselfError = \"You cannot cancel yourself\"\n\tUnableAddError      = \"Unable to add user as friend. Try again later\"\n\tUnableRemoveError   = \"Unable to remove the user. Try again later\"\n\tUnableAcceptError   = \"Unable to accept the request. Try again later\"\n)\n\n// Generic Errors\nconst (\n\tInvalidSession = \"Provided session is invalid\"\n\tServerError    = \"Something went wrong. Try again later\"\n\tUnauthorized   = \"Not Authorized\"\n)\n\n// Message Errors\nconst (\n\tMessageOrFileRequired = \"Either a message or a file is required\"\n\tEditMessageError      = \"Only the author can edit the message\"\n\tDeleteMessageError    = \"Only the author or owner can delete the message\"\n\tDeleteDMMessageError  = \"Only the author can delete the message\"\n)\n"
  },
  {
    "path": "server/model/apperrors/httperrors.go",
    "content": "package apperrors\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// Type holds a type string and integer code for the error\ntype Type string\n\n// \"Set\" of valid errorTypes\nconst (\n\tAuthorization        Type = \"AUTHORIZATION\"        // Authentication Failures -\n\tBadRequest           Type = \"BADREQUEST\"           // Validation errors / BadInput\n\tConflict             Type = \"CONFLICT\"             // Already exists (eg, create account with existent email) - 409\n\tInternal             Type = \"INTERNAL\"             // Server (500) and fallback errors\n\tNotFound             Type = \"NOTFOUND\"             // For not finding resource\n\tPayloadTooLarge      Type = \"PAYLOADTOOLARGE\"      // for uploading tons of JSON, or an image over the limit - 413\n\tServiceUnavailable   Type = \"SERVICE_UNAVAILABLE\"  // For long running handlers\n\tUnsupportedMediaType Type = \"UNSUPPORTEDMEDIATYPE\" // for http 415\n)\n\n// Error holds a custom error for the application\n// which is helpful in returning a consistent\n// error type/message from API endpoints\ntype Error struct {\n\tType    Type   `json:\"type\"`\n\tMessage string `json:\"message\"`\n}\n\n// Error satisfies standard error interface\n// we can return errors from this package as\n// a regular old go _error_\nfunc (e *Error) Error() string {\n\treturn e.Message\n}\n\n// Status is a mapping errors to status codes\n// Of course, this is somewhat redundant since\n// our errors already map http status codes\nfunc (e *Error) Status() int {\n\tswitch e.Type {\n\tcase Authorization:\n\t\treturn http.StatusUnauthorized\n\tcase BadRequest:\n\t\treturn http.StatusBadRequest\n\tcase Conflict:\n\t\treturn http.StatusConflict\n\tcase Internal:\n\t\treturn http.StatusInternalServerError\n\tcase NotFound:\n\t\treturn http.StatusNotFound\n\tcase PayloadTooLarge:\n\t\treturn http.StatusRequestEntityTooLarge\n\tcase ServiceUnavailable:\n\t\treturn http.StatusServiceUnavailable\n\tcase UnsupportedMediaType:\n\t\treturn http.StatusUnsupportedMediaType\n\tdefault:\n\t\treturn http.StatusInternalServerError\n\t}\n}\n\n// Status checks the runtime type\n// of the error and returns an http\n// status code if the error is model.Error\nfunc Status(err error) int {\n\tvar e *Error\n\tif errors.As(err, &e) {\n\t\treturn e.Status()\n\t}\n\treturn http.StatusInternalServerError\n}\n\n/*\n* Error \"Factories\"\n */\n\n// NewAuthorization to create a 401\nfunc NewAuthorization(reason string) *Error {\n\treturn &Error{\n\t\tType:    Authorization,\n\t\tMessage: reason,\n\t}\n}\n\n// NewBadRequest to create 400 errors (validation, for example)\nfunc NewBadRequest(reason string) *Error {\n\treturn &Error{\n\t\tType:    BadRequest,\n\t\tMessage: fmt.Sprintf(\"Bad request. Reason: %v\", reason),\n\t}\n}\n\n// NewConflict to create an error for 409\nfunc NewConflict(name string, value string) *Error {\n\treturn &Error{\n\t\tType:    Conflict,\n\t\tMessage: fmt.Sprintf(\"resource: %v with value: %v already exists\", name, value),\n\t}\n}\n\n// NewInternal for 500 errors and unknown errors\nfunc NewInternal() *Error {\n\treturn &Error{\n\t\tType:    Internal,\n\t\tMessage: ServerError,\n\t}\n}\n\n// NewNotFound to create an error for 404 with a generic error message\nfunc NewNotFound(name string, value string) *Error {\n\treturn &Error{\n\t\tType:    NotFound,\n\t\tMessage: fmt.Sprintf(\"resource: %v with value: %v not found\", name, value),\n\t}\n}\n\n// NewPayloadTooLarge to create an error for 413\nfunc NewPayloadTooLarge(maxBodySize int64, contentLength int64) *Error {\n\treturn &Error{\n\t\tType:    PayloadTooLarge,\n\t\tMessage: fmt.Sprintf(\"Max payload size of %v exceeded. Actual payload size: %v\", maxBodySize, contentLength),\n\t}\n}\n\n// NewServiceUnavailable to create an error for 503\nfunc NewServiceUnavailable() *Error {\n\treturn &Error{\n\t\tType:    ServiceUnavailable,\n\t\tMessage: \"Service unavailable or timed out\",\n\t}\n}\n\n// NewUnsupportedMediaType to create an error for 415\nfunc NewUnsupportedMediaType(reason string) *Error {\n\treturn &Error{\n\t\tType:    UnsupportedMediaType,\n\t\tMessage: reason,\n\t}\n}\n"
  },
  {
    "path": "server/model/base_model.go",
    "content": "package model\n\nimport (\n\t\"time\"\n)\n\n// BaseModel is similar to the gorm.Model and it includes the\n// ID as a string, CreatedAt and UpdatedAt fields\ntype BaseModel struct {\n\tID        string    `gorm:\"primaryKey\" json:\"id\"`\n\tCreatedAt time.Time `gorm:\"index\" json:\"createdAt\"`\n\tUpdatedAt time.Time `json:\"updatedAt\"`\n}\n\n// Success is the default response for successful operation\n// Returns true without the JSON\ntype Success struct {\n\t// Only returns true, not a json object\n\tSuccess bool `json:\"success\"`\n} //@name SuccessResponse\n"
  },
  {
    "path": "server/model/channel.go",
    "content": "package model\n\nimport \"time\"\n\n// Channel represents a text channel in a guild\n// or a text channel for DMs between users.\n// GuildID should only be nil if it is a DM channel\n// PCMembers should only be used if the channel is private.\ntype Channel struct {\n\tBaseModel\n\tGuildID      *string   `gorm:\"index\"`\n\tName         string    `gorm:\"name\"`\n\tIsPublic     bool      `gorm:\"index\"`\n\tIsDM         bool      `gorm:\"is_dm\"`\n\tLastActivity time.Time `gorm:\"autoCreateTime\"`\n\tPCMembers    []User    `gorm:\"many2many:pcmembers;constraint:OnDelete:CASCADE;\"`\n\tMessages     []Message `gorm:\"constraint:OnDelete:CASCADE;\"`\n}\n\n// ChannelResponse is the JSON response of the channel\ntype ChannelResponse struct {\n\tId              string    `json:\"id\"`\n\tName            string    `json:\"name\"`\n\tIsPublic        bool      `json:\"isPublic\"`\n\tCreatedAt       time.Time `json:\"createdAt\"`\n\tUpdatedAt       time.Time `json:\"updatedAt\"`\n\tHasNotification bool      `json:\"hasNotification\"`\n} //@name Channel\n\n// SerializeChannel returns the channel API response.\nfunc (c Channel) SerializeChannel() ChannelResponse {\n\treturn ChannelResponse{\n\t\tId:              c.ID,\n\t\tName:            c.Name,\n\t\tIsPublic:        c.IsPublic,\n\t\tCreatedAt:       c.CreatedAt,\n\t\tUpdatedAt:       c.UpdatedAt,\n\t\tHasNotification: false,\n\t}\n}\n\n// ChannelService defines methods related to channel operations the handler layer expects\n// any service it interacts with to implement\ntype ChannelService interface {\n\tCreateChannel(channel *Channel) (*Channel, error)\n\tGetChannels(userId string, guildId string) (*[]ChannelResponse, error)\n\tGet(channelId string) (*Channel, error)\n\tGetPrivateChannelMembers(channelId string) (*[]string, error)\n\tGetDirectMessages(userId string) (*[]DirectMessage, error)\n\tGetDirectMessageChannel(userId string, memberId string) (*string, error)\n\tGetDMByUserAndChannel(userId string, channelId string) (string, error)\n\tAddDMChannelMembers(memberIds []string, channelId string, userId string) error\n\tSetDirectMessageStatus(dmId string, userId string, isOpen bool) error\n\tDeleteChannel(channel *Channel) error\n\tUpdateChannel(channel *Channel) error\n\tCleanPCMembers(channelId string) error\n\tAddPrivateChannelMembers(memberIds []string, channelId string) error\n\tRemovePrivateChannelMembers(memberIds []string, channelId string) error\n\tIsChannelMember(channel *Channel, userId string) error\n\tOpenDMForAll(dmId string) error\n}\n\n// ChannelRepository defines methods related to channel db operations the service layer expects\n// any repository it interacts with to implement\ntype ChannelRepository interface {\n\tCreate(channel *Channel) (*Channel, error)\n\tGetGuildDefault(guildId string) (*Channel, error)\n\tGet(userId string, guildId string) (*[]ChannelResponse, error)\n\tGetDirectMessages(userId string) (*[]DirectMessage, error)\n\tGetDirectMessageChannel(userId string, memberId string) (*string, error)\n\tGetById(channelId string) (*Channel, error)\n\tGetPrivateChannelMembers(channelId string) (*[]string, error)\n\tAddDMChannelMembers(members []DMMember) error\n\tSetDirectMessageStatus(dmId string, userId string, isOpen bool) error\n\tDeleteChannel(channel *Channel) error\n\tUpdateChannel(channel *Channel) error\n\tCleanPCMembers(channelId string) error\n\tAddPrivateChannelMembers(memberIds []string, channelId string) error\n\tRemovePrivateChannelMembers(memberIds []string, channelId string) error\n\tFindDMByUserAndChannelId(channelId, userId string) (string, error)\n\tOpenDMForAll(dmId string) error\n\tGetDMMemberIds(channelId string) (*[]string, error)\n}\n"
  },
  {
    "path": "server/model/direct_message.go",
    "content": "package model\n\n// DirectMessage is the json response of the channel ID\n// and the other user of the DM.\ntype DirectMessage struct {\n\tId   string `json:\"id\"`\n\tUser DMUser `json:\"user\"`\n} //@name DirectMessage\n\n// DMUser is the other member of the DM.\ntype DMUser struct {\n\tId       string `json:\"id\"`\n\tUsername string `json:\"username\"`\n\tImage    string `json:\"image\"`\n\tIsOnline bool   `json:\"isOnline\"`\n\tIsFriend bool   `json:\"isFriend\"`\n} //@name DMUser\n"
  },
  {
    "path": "server/model/dm_member.go",
    "content": "package model\n\nimport \"time\"\n\n// DMMember represents a member of a DM channel.\n// IsOpen indicates if the DM is open on the client.\ntype DMMember struct {\n\tID        string `gorm:\"primaryKey\"`\n\tUserID    string `gorm:\"primaryKey\"`\n\tChannelId string `gorm:\"primaryKey;\"`\n\tIsOpen    bool\n\tCreatedAt time.Time `gorm:\"index\"`\n\tUpdatedAt time.Time\n}\n"
  },
  {
    "path": "server/model/field_error.go",
    "content": "package model\n\n// ErrorsResponse contains a list of FieldError\ntype ErrorsResponse struct {\n\tErrors []FieldError `json:\"errors\"`\n} //@name ErrorsResponse\n\n// FieldError is used to help extract validation errors\ntype FieldError struct {\n\t// The property containing the error\n\tField string `json:\"field\"`\n\t// The specific error message\n\tMessage string `json:\"message\"`\n} //@name FieldError\n\n// ErrorResponse holds a custom error for the application\ntype ErrorResponse struct {\n\tError HttpError `json:\"error\"`\n} //@name ErrorResponse\n\n// HttpError returns the Http error type and the specific message\ntype HttpError struct {\n\t// The Http Response as a string\n\tType string `json:\"type\"`\n\t// The specific error message\n\tMessage string `json:\"message\"`\n} //@name HttpError\n"
  },
  {
    "path": "server/model/fixture/channel.go",
    "content": "package fixture\n\nimport (\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"time\"\n)\n\n// GetMockChannel returns a mock channel. If guildId is not empty it will set GuildID to that id.\nfunc GetMockChannel(guildId string) *model.Channel {\n\n\tvar guild *string = nil\n\tif guildId != \"\" {\n\t\tguild = &guildId\n\t}\n\n\treturn &model.Channel{\n\t\tBaseModel: model.BaseModel{\n\t\t\tID:        RandID(),\n\t\t\tCreatedAt: time.Now(),\n\t\t\tUpdatedAt: time.Now(),\n\t\t},\n\t\tGuildID:      guild,\n\t\tName:         RandStr(8),\n\t\tIsPublic:     true,\n\t\tLastActivity: time.Now(),\n\t}\n}\n\n// GetMockDMChannel returns a mock channel that has IsDM set to true and does not belong to a guild.\nfunc GetMockDMChannel() *model.Channel {\n\treturn &model.Channel{\n\t\tBaseModel: model.BaseModel{\n\t\t\tID:        RandID(),\n\t\t\tCreatedAt: time.Now(),\n\t\t\tUpdatedAt: time.Now(),\n\t\t},\n\t\tName:         RandID(),\n\t\tIsDM:         true,\n\t\tLastActivity: time.Now(),\n\t}\n}\n"
  },
  {
    "path": "server/model/fixture/faker.go",
    "content": "package fixture\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n)\n\nvar letterRunes = []rune(\"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\")\nvar numberRunes = []rune(\"1234567890\")\n\n// RandStringRunes returns a random latin string of the given length\nfunc RandStringRunes(n int) string {\n\tb := make([]rune, n)\n\tfor i := range b {\n\t\tb[i] = letterRunes[rand.Intn(len(letterRunes))]\n\t}\n\n\treturn string(b)\n}\n\nfunc randNumberRunes(n int) string {\n\tb := make([]rune, n)\n\tfor i := range b {\n\t\tb[i] = numberRunes[rand.Intn(len(numberRunes))]\n\t}\n\n\treturn string(b)\n}\n\nfunc randStringLowerRunes(n int) string {\n\tb := make([]rune, n)\n\tfor i := range b {\n\t\tb[i] = letterRunes[rand.Intn(len(letterRunes)/2)]\n\t}\n\n\treturn string(b)\n}\n\n// RandInt returns a random int within the given range\nfunc RandInt(min, max int) int {\n\treturn rand.Intn(max-min+1) + min\n}\n\n// RandID returns a 15 character long numeric string\nfunc RandID() string {\n\treturn randNumberRunes(15)\n}\n\n// Username returns a random string that's 4 to 15 characters long\nfunc Username() string {\n\treturn RandStringRunes(RandInt(4, 15))\n}\n\n// Email returns a random email ending with @example.com\nfunc Email() string {\n\temail := fmt.Sprintf(\"%s@example.com\", randStringLowerRunes(RandInt(5, 10)))\n\treturn strings.ToLower(email)\n}\n\n// RandStr returns a random string that has the given length\nfunc RandStr(n int) string {\n\treturn RandStringRunes(n)\n}\n\n// generateAvatar returns an gravatar using the md5 hash of the email\nfunc generateAvatar(email string) string {\n\thash := md5.Sum([]byte(email))\n\treturn fmt.Sprintf(\"https://gravatar.com/avatar/%s?d=identicon\", hex.EncodeToString(hash[:]))\n}\n"
  },
  {
    "path": "server/model/fixture/guild.go",
    "content": "package fixture\n\nimport (\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"time\"\n)\n\n// GetMockGuild returns a mock guild with the given uid as the owner.\nfunc GetMockGuild(uid string) *model.Guild {\n\townerId := RandID()\n\tif uid != \"\" {\n\t\townerId = uid\n\t}\n\n\treturn &model.Guild{\n\t\tBaseModel: model.BaseModel{\n\t\t\tID:        RandID(),\n\t\t\tCreatedAt: time.Now(),\n\t\t\tUpdatedAt: time.Now(),\n\t\t},\n\t\tName:    RandStr(8),\n\t\tOwnerId: ownerId,\n\t}\n}\n"
  },
  {
    "path": "server/model/fixture/message.go",
    "content": "package fixture\n\nimport (\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"time\"\n)\n\n// GetMockMessage returns a mock message with the given uid as the owner and the cid as the ChannelId\nfunc GetMockMessage(uid, cid string) *model.Message {\n\ttext := RandStringRunes(100)\n\n\townerId := RandID()\n\tif uid != \"\" {\n\t\townerId = uid\n\t}\n\n\treturn &model.Message{\n\t\tBaseModel: model.BaseModel{\n\t\t\tID:        RandID(),\n\t\t\tCreatedAt: time.Now(),\n\t\t\tUpdatedAt: time.Now(),\n\t\t},\n\t\tText:       &text,\n\t\tUserId:     ownerId,\n\t\tChannelId:  cid,\n\t\tAttachment: nil,\n\t}\n}\n\n// GetMockMessageResponse returns a mock message response with the given uid as the owner and the cid as the ChannelId\nfunc GetMockMessageResponse(uid, cid string) *model.MessageResponse {\n\tmessage := GetMockMessage(uid, cid)\n\tuser := GetMockUser()\n\n\treturn &model.MessageResponse{\n\t\tId:         message.ID,\n\t\tText:       message.Text,\n\t\tCreatedAt:  message.CreatedAt,\n\t\tUpdatedAt:  message.UpdatedAt,\n\t\tAttachment: message.Attachment,\n\t\tUser: model.MemberResponse{\n\t\t\tId:        user.ID,\n\t\t\tUsername:  user.Username,\n\t\t\tImage:     user.Image,\n\t\t\tIsOnline:  user.IsOnline,\n\t\t\tCreatedAt: user.CreatedAt,\n\t\t\tUpdatedAt: user.UpdatedAt,\n\t\t\tNickname:  nil,\n\t\t\tColor:     nil,\n\t\t\tIsFriend:  false,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "server/model/fixture/multipart.go",
    "content": "package fixture\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image\"\n\t\"image/png\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"net/textproto\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n)\n\n// MultipartImage used for instantiating a test fixture\n// for creating multipart file uploads containing an image\ntype MultipartImage struct {\n\timagePath     string\n\tImageFile     *os.File\n\tMultipartBody *bytes.Buffer\n\tContentType   string\n}\n\n// NewMultipartImage creates an image file for testing\n// and creates a Multipart Form with this image file\n// for testing\nfunc NewMultipartImage(fileName string, contentType string) *MultipartImage {\n\t// create test file in same folder as this fixture\n\t_, b, _, _ := runtime.Caller(0)\n\tdir := filepath.Dir(b)\n\n\timagePath := filepath.Join(dir, fileName)\n\n\t// f, _ := os.Open(imagePath)\n\tf := createImage(imagePath)\n\n\tdefer func(f *os.File) {\n\t\t_ = f.Close()\n\t}(f)\n\n\t// create a multipart write onto which we\n\t// will write the image file\n\tbody := &bytes.Buffer{}\n\twriter := multipart.NewWriter(body)\n\n\t// manually create form file as CreateFormFile will\n\t// force file's content type to \"application/octet-stream\"\n\th := make(textproto.MIMEHeader)\n\th.Set(\n\t\t\"Content-Disposition\",\n\t\tfmt.Sprintf(`form-data; name=\"%s\"; filename=\"%s\"`, \"file\", fileName),\n\t)\n\th.Set(\"Content-Type\", contentType)\n\tpart, _ := writer.CreatePart(h)\n\n\t_, _ = io.Copy(part, f)\n\t_ = writer.Close()\n\n\treturn &MultipartImage{\n\t\timagePath:     imagePath,\n\t\tImageFile:     f,\n\t\tMultipartBody: body,\n\t\tContentType:   writer.FormDataContentType(),\n\t}\n}\n\n// GetFormFile extracts form file from multipart body\nfunc (m *MultipartImage) GetFormFile() *multipart.FileHeader {\n\t_, params, _ := mime.ParseMediaType(m.ContentType)\n\tmr := multipart.NewReader(m.MultipartBody, params[\"boundary\"])\n\tform, _ := mr.ReadForm(1024)\n\tfiles := form.File[\"file\"]\n\n\treturn files[0]\n}\n\n// Close removes created file for test\nfunc (m *MultipartImage) Close() {\n\t_ = m.ImageFile.Close()\n\t_ = os.Remove(m.imagePath)\n}\n\n// createImage used to create a quick example\n// 1px x 1px image encoded as a PNG\nfunc createImage(imagePath string) *os.File {\n\trect := image.Rect(0, 0, 1, 1)\n\timg := image.NewRGBA(rect)\n\n\tf, _ := os.Create(imagePath)\n\t_ = png.Encode(f, img)\n\n\treturn f\n}\n"
  },
  {
    "path": "server/model/fixture/user.go",
    "content": "package fixture\n\nimport (\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"time\"\n)\n\n// GetMockUser returns a mock user\nfunc GetMockUser() *model.User {\n\temail := Email()\n\treturn &model.User{\n\t\tBaseModel: model.BaseModel{\n\t\t\tID:        RandID(),\n\t\t\tCreatedAt: time.Now(),\n\t\t\tUpdatedAt: time.Now(),\n\t\t},\n\t\tUsername: Username(),\n\t\tEmail:    email,\n\t\tPassword: RandStr(8),\n\t\tImage:    generateAvatar(email),\n\t}\n}\n"
  },
  {
    "path": "server/model/friend.go",
    "content": "package model\n\n// Friend represents the api response of a user's friend.\ntype Friend struct {\n\tId       string `json:\"id\"`\n\tUsername string `json:\"username\"`\n\tImage    string `json:\"image\"`\n\tIsOnline bool   `json:\"isOnline\"`\n} //@name Friend\n\n// FriendService defines methods related to friend operations the handler layer expects\n// any service it interacts with to implement\ntype FriendService interface {\n\tGetFriends(id string) (*[]Friend, error)\n\tGetRequests(id string) (*[]FriendRequest, error)\n\tGetMemberById(id string) (*User, error)\n\tDeleteRequest(memberId string, userId string) error\n\tRemoveFriend(memberId string, userId string) error\n\tSaveRequests(user *User) error\n}\n\n// FriendRepository defines methods related to friend db operations the service layer expects\n// any repository it interacts with to implement\ntype FriendRepository interface {\n\tFindByID(id string) (*User, error)\n\tFriendsList(id string) (*[]Friend, error)\n\tRequestList(id string) (*[]FriendRequest, error)\n\tDeleteRequest(memberId string, userId string) error\n\tRemoveFriend(memberId string, userId string) error\n\tSave(user *User) error\n}\n"
  },
  {
    "path": "server/model/friend_request.go",
    "content": "package model\n\n// RequestType stands for the type of friend request\ntype RequestType int\n\n// FriendRequest RequestType enum\nconst (\n\tOutgoing RequestType = iota\n\tIncoming\n)\n\n// FriendRequest contains all info to display request.\n// Type stands for the type of the request.\n// 1: Incoming,\n// 0: Outgoing\ntype FriendRequest struct {\n\tId       string `json:\"id\"`\n\tUsername string `json:\"username\"`\n\tImage    string `json:\"image\"`\n\t// 1: Incoming, 0: Outgoing\n\tType RequestType `json:\"type\" enums:\"0,1\"`\n} //@name FriendRequest\n"
  },
  {
    "path": "server/model/guild.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"github.com/lib/pq\"\n\t\"time\"\n)\n\n// Guild represents the server many users can chat in.\ntype Guild struct {\n\tBaseModel\n\tName        string `gorm:\"not null\"`\n\tOwnerId     string `gorm:\"not null\"`\n\tIcon        *string\n\tInviteLinks pq.StringArray `gorm:\"type:text[]\"`\n\tMembers     []User         `gorm:\"many2many:members;constraint:OnDelete:CASCADE;\"`\n\tChannels    []Channel      `gorm:\"constraint:OnDelete:CASCADE;\"`\n\tBans        []User         `gorm:\"many2many:bans;constraint:OnDelete:CASCADE;\"`\n\tVCMembers   []User         `gorm:\"many2many:vc_members;constraint:OnDelete:CASCADE;\"`\n}\n\n// GuildResponse contains all info to display a guild.\n// The DefaultChannelId is the channel the user first gets directed to\n// and is the oldest channel of the guild.\ntype GuildResponse struct {\n\tId               string    `json:\"id\"`\n\tName             string    `json:\"name\"`\n\tOwnerId          string    `json:\"ownerId\"`\n\tIcon             *string   `json:\"icon\"`\n\tCreatedAt        time.Time `json:\"createdAt\"`\n\tUpdatedAt        time.Time `json:\"updatedAt\"`\n\tHasNotification  bool      `json:\"hasNotification\"`\n\tDefaultChannelId string    `json:\"default_channel_id\"`\n} //@name GuildResponse\n\n// SerializeGuild returns the guild API response.\n// The DefaultChannelId represents the default channel the user gets send to.\nfunc (g Guild) SerializeGuild(channelId string) GuildResponse {\n\treturn GuildResponse{\n\t\tId:               g.ID,\n\t\tName:             g.Name,\n\t\tOwnerId:          g.OwnerId,\n\t\tIcon:             g.Icon,\n\t\tCreatedAt:        g.CreatedAt,\n\t\tUpdatedAt:        g.UpdatedAt,\n\t\tHasNotification:  false,\n\t\tDefaultChannelId: channelId,\n\t}\n}\n\n// GuildService defines methods related to guild operations the handler layer expects\n// any service it interacts with to implement\ntype GuildService interface {\n\tGetUser(uid string) (*User, error)\n\tGetGuild(id string) (*Guild, error)\n\tGetUserGuilds(uid string) (*[]GuildResponse, error)\n\tGetGuildMembers(userId string, guildId string) (*[]MemberResponse, error)\n\tGetVCMembers(guildId string) (*[]VCMemberResponse, error)\n\tCreateGuild(guild *Guild) (*Guild, error)\n\tGenerateInviteLink(ctx context.Context, guildId string, isPermanent bool) (string, error)\n\tUpdateGuild(guild *Guild) error\n\tGetGuildIdFromInvite(ctx context.Context, token string) (string, error)\n\tGetDefaultChannel(guildId string) (*Channel, error)\n\tInvalidateInvites(ctx context.Context, guild *Guild)\n\tRemoveMember(userId string, guildId string) error\n\tUnbanMember(userId string, guildId string) error\n\tDeleteGuild(guildId string) error\n\tGetBanList(guildId string) (*[]BanResponse, error)\n\tGetMemberSettings(userId string, guildId string) (*MemberSettings, error)\n\tUpdateMemberSettings(settings *MemberSettings, userId string, guildId string) error\n\tFindUsersByIds(ids []string, guildId string) (*[]User, error)\n\tUpdateMemberLastSeen(userId, guildId string) error\n\tRemoveVCMember(userId, guildId string) error\n\tUpdateVCMember(isMuted, isDeafened bool, userId, guildId string) error\n\tGetVCMember(userId, guildId string) (*VCMember, error)\n}\n\n// GuildRepository defines methods related to guild db operations the service layer expects\n// any repository it interacts with to implement\ntype GuildRepository interface {\n\tFindUserByID(uid string) (*User, error)\n\tFindByID(id string) (*Guild, error)\n\tList(uid string) (*[]GuildResponse, error)\n\tGuildMembers(userId string, guildId string) (*[]MemberResponse, error)\n\tVCMembers(guildId string) (*[]VCMemberResponse, error)\n\tCreate(guild *Guild) (*Guild, error)\n\tSave(guild *Guild) error\n\tRemoveMember(userId string, guildId string) error\n\tDelete(guildId string) error\n\tUnbanMember(userId string, guildId string) error\n\tGetBanList(guildId string) (*[]BanResponse, error)\n\tGetMemberSettings(userId string, guildId string) (*MemberSettings, error)\n\tUpdateMemberSettings(settings *MemberSettings, userId string, guildId string) error\n\tFindUsersByIds(ids []string, guildId string) (*[]User, error)\n\tGetMember(userId, guildId string) (*User, error)\n\tUpdateMemberLastSeen(userId, guildId string) error\n\tRemoveVCMember(userId, guildId string) error\n\tGetMemberIds(guildId string) (*[]string, error)\n\tUpdateVCMember(isMuted, isDeafened bool, userId, guildId string) error\n\tGetVCMember(userId, guildId string) (*VCMember, error)\n}\n"
  },
  {
    "path": "server/model/interfaces.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"mime/multipart\"\n)\n\n// FileRepository defines methods related to file upload the service layer expects\n// any repository it interacts with to implement\ntype FileRepository interface {\n\tUploadAvatar(header *multipart.FileHeader, directory string) (string, error)\n\tUploadFile(header *multipart.FileHeader, directory, filename, mimetype string) (string, error)\n\tDeleteImage(key string) error\n}\n\n// MailRepository defines methods related to mail operations the service layer expects\n// any repository it interacts with to implement\ntype MailRepository interface {\n\tSendResetMail(email string, html string) error\n}\n\n// RedisRepository defines methods related to the redis db the service layer expects\n// any repository it interacts with to implement\ntype RedisRepository interface {\n\tSetResetToken(ctx context.Context, id string) (string, error)\n\tGetIdFromToken(ctx context.Context, token string) (string, error)\n\tSaveInvite(ctx context.Context, guildId string, id string, isPermanent bool) error\n\tGetInvite(ctx context.Context, token string) (string, error)\n\tInvalidateInvites(ctx context.Context, guild *Guild)\n}\n"
  },
  {
    "path": "server/model/invite.go",
    "content": "package model\n\n// Invite represents an invite link for a guild\n// IsPermanent indicates if the invite should not expire\ntype Invite struct {\n\tGuildId     string `json:\"guild_id\"`\n\tIsPermanent bool   `json:\"is_permanent\"`\n}\n"
  },
  {
    "path": "server/model/member.go",
    "content": "package model\n\nimport \"time\"\n\n// Member represents a user in a guild and is the join table between\n// User and Guild.\ntype Member struct {\n\tUserID    string    `gorm:\"primaryKey;constraint:OnDelete:CASCADE;\"`\n\tGuildID   string    `gorm:\"primaryKey;constraint:OnDelete:CASCADE;\"`\n\tNickname  *string   `gorm:\"nickname\"`\n\tColor     *string   `gorm:\"color\"`\n\tLastSeen  time.Time `gorm:\"autoCreateTime\"`\n\tCreatedAt time.Time `gorm:\"index\"`\n\tUpdatedAt time.Time\n}\n\ntype VCMember struct {\n\tUserID     string `gorm:\"primaryKey;constraint:OnDelete:CASCADE;\"`\n\tGuildID    string `gorm:\"primaryKey;constraint:OnDelete:CASCADE;\"`\n\tIsMuted    bool\n\tIsDeafened bool\n}\n\n// MemberResponse is the API response of a member.\ntype MemberResponse struct {\n\tId        string    `json:\"id\"`\n\tUsername  string    `json:\"username\"`\n\tImage     string    `json:\"image\"`\n\tIsOnline  bool      `json:\"isOnline\"`\n\tCreatedAt time.Time `json:\"createdAt\"`\n\tUpdatedAt time.Time `json:\"updatedAt\"`\n\tNickname  *string   `json:\"nickname\"`\n\tColor     *string   `json:\"color\"`\n\tIsFriend  bool      `json:\"isFriend\"`\n} //@name Member\n\n// BanResponse is the API response of a banned member.\ntype BanResponse struct {\n\tId       string `json:\"id\"`\n\tUsername string `json:\"username\"`\n\tImage    string `json:\"image\"`\n} //@name BanResponse\n\n// MemberSettings is the API response of a member's guild settings.\ntype MemberSettings struct {\n\tNickname *string `json:\"nickname\"`\n\tColor    *string `json:\"color\"`\n} //@name MemberSettings\n\n// VCMemberResponse is the API response of a member that is currently in a VC.\ntype VCMemberResponse struct {\n\tId         string  `json:\"id\"`\n\tUsername   string  `json:\"username\"`\n\tImage      string  `json:\"image\"`\n\tIsMuted    bool    `json:\"isMuted\"`\n\tIsDeafened bool    `json:\"IsDeafened\"`\n\tNickname   *string `json:\"nickname\"`\n} //@name VCMember\n"
  },
  {
    "path": "server/model/message.go",
    "content": "package model\n\nimport (\n\t\"mime/multipart\"\n\t\"time\"\n)\n\n// Message represents a text message in a channel.\n// It may contain an Attachment that is displayed instead of text.\ntype Message struct {\n\tBaseModel\n\tText       *string\n\tUserId     string      `gorm:\"index;constraint:OnDelete:CASCADE;\"`\n\tChannelId  string      `gorm:\"index;constraint:OnDelete:CASCADE;\"`\n\tAttachment *Attachment `gorm:\"constraint:OnDelete:CASCADE;\"`\n}\n\n// MessageResponse is the API response of a Message\ntype MessageResponse struct {\n\tId         string         `json:\"id\"`\n\tText       *string        `json:\"text\"`\n\tCreatedAt  time.Time      `json:\"createdAt\"`\n\tUpdatedAt  time.Time      `json:\"updatedAt\"`\n\tAttachment *Attachment    `json:\"attachment\"`\n\tUser       MemberResponse `json:\"user\"`\n} //@name Message\n\n// Attachment represents a message attachment that displays\n// a file instead of text.\ntype Attachment struct {\n\tID        string    `gorm:\"primaryKey\" json:\"-\"`\n\tCreatedAt time.Time `json:\"-\"`\n\tUpdatedAt time.Time `json:\"-\"`\n\tUrl       string    `json:\"url\"`\n\tFileType  string    `json:\"filetype\"`\n\tFilename  string    `json:\"filename\"`\n\tMessageId string    `gorm:\"index;constraint:OnDelete:CASCADE;\" json:\"-\"`\n} //@name Attachment\n\n// MessageService defines methods related to message operations the handler layer expects\n// any service it interacts with to implement\ntype MessageService interface {\n\tGetMessages(userId string, channel *Channel, cursor string) (*[]MessageResponse, error)\n\tCreateMessage(params *Message) (*Message, error)\n\tUpdateMessage(message *Message) error\n\tDeleteMessage(message *Message) error\n\tUploadFile(header *multipart.FileHeader, channelId string) (*Attachment, error)\n\tGet(messageId string) (*Message, error)\n}\n\n// MessageRepository defines methods related message db operations the service layer expects\n// any repository it interacts with to implement\ntype MessageRepository interface {\n\tGetMessages(userId string, channel *Channel, cursor string) (*[]MessageResponse, error)\n\tCreateMessage(params *Message) (*Message, error)\n\tUpdateMessage(message *Message) error\n\tDeleteMessage(message *Message) error\n\tGetById(messageId string) (*Message, error)\n}\n"
  },
  {
    "path": "server/model/user.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"mime/multipart\"\n)\n\n// User represents the user of the website.\ntype User struct {\n\tBaseModel\n\tUsername string    `gorm:\"not null\" json:\"username\"`\n\tEmail    string    `gorm:\"not null;uniqueIndex\" json:\"email\"`\n\tPassword string    `gorm:\"not null\" json:\"-\"`\n\tImage    string    `json:\"image\"`\n\tIsOnline bool      `gorm:\"index;default:true\" json:\"isOnline\"`\n\tFriends  []User    `gorm:\"many2many:friends;\" json:\"-\"`\n\tRequests []User    `gorm:\"many2many:friend_requests;joinForeignKey:sender_id;joinReferences:receiver_id\" json:\"-\"`\n\tGuilds   []Guild   `gorm:\"many2many:members;\" json:\"-\"`\n\tMessage  []Message `json:\"-\"`\n} //@name User\n\n// UserService defines methods related to account operations the handler layer expects\n// any service it interacts with to implement\ntype UserService interface {\n\tGet(id string) (*User, error)\n\tGetByEmail(email string) (*User, error)\n\tRegister(user *User) (*User, error)\n\tLogin(email, password string) (*User, error)\n\tUpdateAccount(user *User) error\n\tIsEmailAlreadyInUse(email string) bool\n\tChangeAvatar(header *multipart.FileHeader, directory string) (string, error)\n\tDeleteImage(key string) error\n\tChangePassword(currentPassword, newPassword string, user *User) error\n\tForgotPassword(ctx context.Context, user *User) error\n\tResetPassword(ctx context.Context, password string, token string) (*User, error)\n\tGetFriendAndGuildIds(userId string) (*[]string, error)\n\tGetRequestCount(userId string) (*int64, error)\n}\n\n// UserRepository defines methods related to account db operations the service layer expects\n// any repository it interacts with to implement\ntype UserRepository interface {\n\tFindByID(id string) (*User, error)\n\tCreate(user *User) (*User, error)\n\tFindByEmail(email string) (*User, error)\n\tUpdate(user *User) error\n\tGetFriendAndGuildIds(userId string) (*[]string, error)\n\tGetRequestCount(userId string) (*int64, error)\n}\n"
  },
  {
    "path": "server/model/ws_message.go",
    "content": "package model\n\nimport (\n\t\"encoding/json\"\n\t\"log\"\n)\n\n// ReceivedMessage represents a received websocket message\ntype ReceivedMessage struct {\n\tAction  string `json:\"action\"`\n\tRoom    string `json:\"room\"`\n\tMessage *any   `json:\"message\"`\n}\n\n// WebsocketMessage represents an emitted message\ntype WebsocketMessage struct {\n\tAction string `json:\"action\"`\n\tData   any    `json:\"data\"`\n}\n\n// Encode turns the message into a byte array\nfunc (message *WebsocketMessage) Encode() []byte {\n\tencoding, err := json.Marshal(message)\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\treturn encoding\n}\n\n// SocketService defines methods related emitting websockets events the service layer expects\n// any repository it interacts with to implement\ntype SocketService interface {\n\tEmitNewMessage(room string, message *MessageResponse)\n\tEmitEditMessage(room string, message *MessageResponse)\n\tEmitDeleteMessage(room, messageId string)\n\n\tEmitNewChannel(room string, channel *ChannelResponse)\n\tEmitNewPrivateChannel(members []string, channel *ChannelResponse)\n\tEmitEditChannel(room string, channel *ChannelResponse)\n\tEmitDeleteChannel(channel *Channel)\n\n\tEmitEditGuild(guild *Guild)\n\tEmitDeleteGuild(guildId string, members []string)\n\tEmitRemoveFromGuild(memberId, guildId string)\n\n\tEmitAddMember(room string, member *User)\n\tEmitRemoveMember(room, memberId string)\n\n\tEmitNewDMNotification(channelId string, user *User)\n\tEmitNewNotification(guildId, channelId string)\n\n\tEmitSendRequest(room string)\n\tEmitAddFriendRequest(room string, request *FriendRequest)\n\tEmitAddFriend(user, member *User)\n\tEmitRemoveFriend(userId, memberId string)\n}\n"
  },
  {
    "path": "server/repository/channel_repository.go",
    "content": "package repository\n\nimport (\n\t\"database/sql\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"gorm.io/gorm\"\n\t\"log\"\n\t\"time\"\n)\n\n// channelRepository is data/repository implementation\n// of service layer ChannelRepository\ntype channelRepository struct {\n\tDB *gorm.DB\n}\n\n// NewChannelRepository is a factory for initializing Channel Repositories\nfunc NewChannelRepository(db *gorm.DB) model.ChannelRepository {\n\treturn &channelRepository{\n\t\tDB: db,\n\t}\n}\n\n// Create inserts a channel in the DB\nfunc (r *channelRepository) Create(channel *model.Channel) (*model.Channel, error) {\n\tif result := r.DB.Create(&channel); result.Error != nil {\n\t\tlog.Printf(\"Could not create a channel for guild: %v. Reason: %v\\n\", channel.GuildID, result.Error)\n\t\treturn nil, apperrors.NewInternal()\n\t}\n\n\treturn channel, nil\n}\n\n// GetGuildDefault fetches the oldest channel for the given guildId from the DB\nfunc (r *channelRepository) GetGuildDefault(guildId string) (*model.Channel, error) {\n\tchannel := model.Channel{}\n\tresult := r.DB.\n\t\tWhere(\"guild_id = ?\", guildId).\n\t\tOrder(\"created_at ASC\").\n\t\tFirst(&channel)\n\n\treturn &channel, result.Error\n}\n\n// Get fetches all public channels for the given guildId\n// and the private channels the given user is part in\nfunc (r *channelRepository) Get(userId string, guildId string) (*[]model.ChannelResponse, error) {\n\tvar channels []model.ChannelResponse\n\n\tresult := r.DB.\n\t\tRaw(`\n\t\t\tSELECT DISTINCT ON (c.id, c.\"created_at\") c.id, c.name, \n\t\t\tc.\"is_public\", c.\"created_at\", c.\"updated_at\",\n\t\t\t(c.\"last_activity\" > m.\"last_seen\") AS \"hasNotification\"\n\t\t\tFROM channels AS c\n\t\t\tLEFT OUTER JOIN pcmembers as pc\n\t\t\tON c.\"id\"::text = pc.\"channel_id\"::text\n\t\t\tLEFT OUTER JOIN members m on c.\"guild_id\" = m.\"guild_id\"\n\t\t\tWHERE c.\"guild_id\"::text = ?\n\t\t\tAND (c.\"is_public\" = true or pc.\"user_id\"::text = ?)\n\t\t\tORDER BY c.\"created_at\"\n\t\t`, guildId, userId).\n\t\tScan(&channels)\n\n\treturn &channels, result.Error\n}\n\n// dmQuery represents the fetched fields for GetDirectMessages\ntype dmQuery struct {\n\tChannelId string\n\tId        string\n\tUsername  string\n\tImage     string\n\tIsOnline  bool\n\tIsFriend  bool\n}\n\n// GetDirectMessages returns all DMs for the given user\nfunc (r *channelRepository) GetDirectMessages(userId string) (*[]model.DirectMessage, error) {\n\tvar results []dmQuery\n\n\terr := r.DB.\n\t\tRaw(`\n\t\t\tSELECT dm.\"channel_id\", u.username, u.image, u.id, u.\"is_online\", u.\"created_at\", u.\"updated_at\"\n\t\t\tFROM users u\n\t\t\tJOIN dm_members dm ON dm.\"user_id\" = u.id\n\t\t\tWHERE u.id != @id\n\t\t\tAND dm.\"channel_id\" IN (\n\t\t\t\tSELECT DISTINCT c.id\n\t\t\t\tFROM channels as c\n\t\t\t\tLEFT OUTER JOIN dm_members as dm\n\t\t\t\tON c.\"id\" = dm.\"channel_id\"\n\t\t\t\tJOIN users u on dm.\"user_id\" = u.id\n\t\t\t\tWHERE c.\"is_public\" = false\n\t\t\t\tAND c.is_dm = true\n\t\t\t\tAND dm.\"is_open\" = true\n\t\t\t\tAND dm.\"user_id\" = @id\n\t\t\t)\n\t\t\torder by dm.\"updated_at\" DESC \n\t\t`, sql.Named(\"id\", userId)).\n\t\tScan(&results)\n\n\tvar channels []model.DirectMessage\n\n\t// Turn into DirectMessage response\n\tfor _, dm := range results {\n\t\tchannel := model.DirectMessage{\n\t\t\tId: dm.ChannelId,\n\t\t\tUser: model.DMUser{\n\t\t\t\tId:       dm.Id,\n\t\t\t\tUsername: dm.Username,\n\t\t\t\tImage:    dm.Image,\n\t\t\t\tIsOnline: dm.IsOnline,\n\t\t\t\tIsFriend: dm.IsFriend,\n\t\t\t},\n\t\t}\n\t\tchannels = append(channels, channel)\n\t}\n\n\treturn &channels, err.Error\n}\n\n// GetDirectMessageChannel returns the dm channel ID of the given members\n// if it exists.\nfunc (r *channelRepository) GetDirectMessageChannel(userId string, memberId string) (*string, error) {\n\tvar id string\n\n\tresult := r.DB.\n\t\tRaw(`\n\t\t\tSELECT c.id\n\t\t\tFROM channels as c, dm_members dm \n\t\t\tWHERE dm.\"channel_id\" = c.\"id\" AND c.is_dm = true AND c.\"is_public\" = false\n\t\t\tGROUP BY c.\"id\"\n\t\t\tHAVING array_agg(dm.\"user_id\"::text) @> Array[?,?]\n\t\t\tAND count(dm.\"user_id\") = 2;\n\t\t`, userId, memberId).\n\t\tScan(&id)\n\n\treturn &id, result.Error\n}\n\n// GetById returns the channel with its PCMembers for the given channel id\nfunc (r *channelRepository) GetById(channelId string) (*model.Channel, error) {\n\tvar channel model.Channel\n\terr := r.DB.Preload(\"PCMembers\").Where(\"id = ?\", channelId).First(&channel).Error\n\treturn &channel, err\n}\n\n// GetPrivateChannelMembers returns the ids of all users\n// that are members of the given channel\nfunc (r *channelRepository) GetPrivateChannelMembers(channelId string) (*[]string, error) {\n\tvar members []string\n\terr := r.DB.\n\t\tRaw(\"SELECT pc.user_id FROM pcmembers pc JOIN channels c ON pc.channel_id = c.id WHERE c.id = ?\", channelId).\n\t\tScan(&members).Error\n\treturn &members, err\n}\n\n// AddDMChannelMembers inserts the given DM members in the DB\nfunc (r *channelRepository) AddDMChannelMembers(members []model.DMMember) error {\n\tif err := r.DB.CreateInBatches(&members, len(members)).Error; err != nil {\n\t\tlog.Printf(\"Could not add members to DM. Reason: %v\\n\", err)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\n// SetDirectMessageStatus opens or closes the dm channel for the given\n// userId\nfunc (r *channelRepository) SetDirectMessageStatus(dmId string, userId string, isOpen bool) error {\n\terr := r.DB.\n\t\tTable(\"dm_members\").\n\t\tWhere(\"channel_id = ? AND user_id = ?\", dmId, userId).\n\t\tUpdates(map[string]any{\n\t\t\t\"is_open\":    isOpen,\n\t\t\t\"updated_at\": time.Now(),\n\t\t}).\n\t\tError\n\treturn err\n}\n\n// OpenDMForAll opens the given dm channel for all users in the channel\nfunc (r *channelRepository) OpenDMForAll(dmId string) error {\n\terr := r.DB.\n\t\tTable(\"dm_members\").\n\t\tWhere(\"channel_id = ? \", dmId).\n\t\tUpdates(map[string]any{\n\t\t\t\"is_open\":    true,\n\t\t\t\"updated_at\": time.Now(),\n\t\t}).\n\t\tError\n\treturn err\n}\n\n// DeleteChannel deletes the given channel from the DB\nfunc (r *channelRepository) DeleteChannel(channel *model.Channel) error {\n\tif result := r.DB.Delete(&channel); result.Error != nil {\n\t\tlog.Printf(\"Could not delete the channel with id: %v. Reason: %v\\n\", channel, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\n// UpdateChannel updates the given channel in the DB\nfunc (r *channelRepository) UpdateChannel(channel *model.Channel) error {\n\tif result := r.DB.Save(&channel); result.Error != nil {\n\t\tlog.Printf(\"Could not update the given channel: %v. Reason: %v\\n\", channel.ID, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\n// CleanPCMembers removes all private channel members from the given channel\nfunc (r *channelRepository) CleanPCMembers(channelId string) error {\n\tif result := r.DB.Exec(\"DELETE FROM pcmembers WHERE channel_id = ?\", channelId); result.Error != nil {\n\t\tlog.Printf(\"Could not clean members from the channel with id: %v. Reason: %v\\n\", channelId, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\n// AddPrivateChannelMembers inserts the given member as PCMembers in the given channel\nfunc (r *channelRepository) AddPrivateChannelMembers(memberIds []string, channelId string) error {\n\tvar err error = nil\n\tfor _, id := range memberIds {\n\t\terr = r.DB.Exec(\"INSERT INTO pcmembers VALUES (?, ?)\", channelId, id).Error\n\t}\n\n\tif err != nil {\n\t\tlog.Printf(\"Could not add members to private channel %s. Reason: %v\\n\", channelId, err)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\n// RemovePrivateChannelMembers removes the given ids from the PCMembers of the given channel\nfunc (r *channelRepository) RemovePrivateChannelMembers(memberIds []string, channelId string) error {\n\tif err := r.DB.Exec(\"DELETE FROM pcmembers WHERE channel_id = ? AND user_id IN ?\", channelId, memberIds).\n\t\tError; err != nil {\n\t\tlog.Printf(\"Could not remove members from private channel %s. Reason: %v\\n\", channelId, err)\n\t\treturn apperrors.NewInternal()\n\t}\n\n\treturn nil\n}\n\n// FindDMByUserAndChannelId returns the id of dm channel for the given channelId and userId\nfunc (r *channelRepository) FindDMByUserAndChannelId(channelId, userId string) (string, error) {\n\tvar id string\n\terr := r.DB.\n\t\tRaw(\"SELECT id FROM dm_members WHERE user_id = ? AND channel_id = ?\", userId, channelId).\n\t\tScan(&id).Error\n\treturn id, err\n}\n\n// GetDMMemberIds returns the ids of all dm members for the given channel\nfunc (r *channelRepository) GetDMMemberIds(channelId string) (*[]string, error) {\n\tvar members []string\n\terr := r.DB.\n\t\tRaw(\"SELECT u.id FROM users u JOIN dm_members dm on u.id = dm.user_id WHERE dm.channel_id = ?\", channelId).\n\t\tScan(&members).Error\n\treturn &members, err\n}\n"
  },
  {
    "path": "server/repository/file_repository.go",
    "content": "package repository\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"github.com/aws/aws-sdk-go/aws\"\n\t\"github.com/aws/aws-sdk-go/aws/session\"\n\t\"github.com/aws/aws-sdk-go/service/s3\"\n\t\"github.com/aws/aws-sdk-go/service/s3/s3manager\"\n\t\"github.com/disintegration/imaging\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/service\"\n\t\"image\"\n\t\"image/jpeg\"\n\t\"log\"\n\n\t// Register accepted file type jpeg\n\t_ \"image/jpeg\"\n\t// Register accepted file type png\n\t_ \"image/png\"\n\t\"mime/multipart\"\n)\n\n// s3FileRepository includes the S3 session and the BucketName\ntype s3FileRepository struct {\n\tS3Session  *session.Session\n\tBucketName string\n}\n\n// NewFileRepository is a factory for initializing the FileRepository\nfunc NewFileRepository(session *session.Session, bucketName string) model.FileRepository {\n\treturn &s3FileRepository{\n\t\tS3Session:  session,\n\t\tBucketName: bucketName,\n\t}\n}\n\n// UploadAvatar uploads the given image to the initialized Bucket.\n// The image gets resized before being uploaded.\n// All images turn into jpeg images.\n// It returns the url of the uploaded file.\nfunc (s *s3FileRepository) UploadAvatar(header *multipart.FileHeader, directory string) (string, error) {\n\tuploader := s3manager.NewUploader(s.S3Session)\n\n\tid := service.GenerateId()\n\tkey := fmt.Sprintf(\"files/%s/%s.jpeg\", directory, id)\n\n\tfile, err := header.Open()\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to open header: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tsrc, _, err := image.Decode(file)\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to decode image: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\timg := imaging.Resize(src, 150, 0, imaging.Lanczos)\n\n\tbuf := new(bytes.Buffer)\n\terr = jpeg.Encode(buf, img, &jpeg.Options{Quality: 75})\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to encode image: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tup, err := uploader.Upload(&s3manager.UploadInput{\n\t\tBody:        buf,\n\t\tBucket:      aws.String(s.BucketName),\n\t\tContentType: aws.String(\"image/jpeg\"),\n\t\tKey:         aws.String(key),\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to upload file: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tif err = file.Close(); err != nil {\n\t\tlog.Printf(\"Failed to close file: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\treturn up.Location, nil\n}\n\n// UploadFile uploads the given file to the initialized Bucket.\n// It returns the url of the uploaded file.\nfunc (s *s3FileRepository) UploadFile(header *multipart.FileHeader, directory, filename, mimetype string) (string, error) {\n\tuploader := s3manager.NewUploader(s.S3Session)\n\n\tkey := fmt.Sprintf(\"files/%s/%s\", directory, filename)\n\n\tfile, err := header.Open()\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to open header: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tup, err := uploader.Upload(&s3manager.UploadInput{\n\t\tBody:        file,\n\t\tBucket:      aws.String(s.BucketName),\n\t\tContentType: aws.String(mimetype),\n\t\tKey:         aws.String(key),\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to upload file: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tif err = file.Close(); err != nil {\n\t\tlog.Printf(\"Failed to close file: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\treturn up.Location, nil\n}\n\n// DeleteImage deletes the file from the Bucket.\nfunc (s *s3FileRepository) DeleteImage(key string) error {\n\tsrv := s3.New(s.S3Session)\n\t_, err := srv.DeleteObject(&s3.DeleteObjectInput{\n\t\tBucket: aws.String(s.BucketName),\n\t\tKey:    aws.String(key),\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to delete image: %v\\n\", err.Error())\n\t\treturn apperrors.NewInternal()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "server/repository/friend_repository.go",
    "content": "package repository\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"gorm.io/gorm\"\n)\n\n// friendRepository is data/repository implementation\n// of service layer FriendRepository\ntype friendRepository struct {\n\tDB *gorm.DB\n}\n\n// NewFriendRepository is a factory for initializing Friend Repositories\nfunc NewFriendRepository(db *gorm.DB) model.FriendRepository {\n\treturn &friendRepository{\n\t\tDB: db,\n\t}\n}\n\n// FriendsList returns the friends for the given user ID from the DB\nfunc (r *friendRepository) FriendsList(id string) (*[]model.Friend, error) {\n\tvar friends []model.Friend\n\n\tresult := r.DB.\n\t\tTable(\"users\").\n\t\tJoins(`JOIN friends ON friends.user_id = \"users\".id`).\n\t\tWhere(\"friends.friend_id = ?\", id).\n\t\tFind(&friends)\n\n\treturn &friends, result.Error\n}\n\n// RequestList returns the friend requests for the given user ID from the DB\nfunc (r *friendRepository) RequestList(id string) (*[]model.FriendRequest, error) {\n\tvar requests []model.FriendRequest\n\n\tresult := r.DB.\n\t\tRaw(`\n\t\t  select u.id, u.username, u.image, 1 as \"type\" from users u\n\t\t  join friend_requests fr on u.id = fr.\"sender_id\"\n\t\t  where fr.\"receiver_id\" = @id\n\t\t  UNION\n\t\t  select u.id, u.username, u.image, 0 as \"type\" from users u\n\t\t  join friend_requests fr on u.id = fr.\"receiver_id\"\n\t\t  where fr.\"sender_id\" = @id\n\t\t  order by username;\n\t\t`, sql.Named(\"id\", id)).\n\t\tFind(&requests)\n\n\treturn &requests, result.Error\n}\n\n// FindByID returns a User containing their friends and requests from the DB\nfunc (r *friendRepository) FindByID(id string) (*model.User, error) {\n\tuser := &model.User{}\n\n\tif err := r.DB.\n\t\tPreload(\"Friends\").\n\t\tPreload(\"Requests\").\n\t\tWhere(\"id = ?\", id).\n\t\tFirst(&user).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn user, apperrors.NewNotFound(\"uid\", id)\n\t\t}\n\t\treturn user, apperrors.NewInternal()\n\t}\n\n\treturn user, nil\n}\n\n// DeleteRequest removes the given member and user from the friend requests\nfunc (r *friendRepository) DeleteRequest(memberId string, userId string) error {\n\treturn r.DB.Exec(`\n\t\tDELETE\n\t\tFROM friend_requests\n\t\tWHERE receiver_id = @memberId AND sender_id = @userId\n\t\t   OR receiver_id = @userId AND sender_id = @memberId\n`, sql.Named(\"memberId\", memberId), sql.Named(\"userId\", userId)).Error\n}\n\n// RemoveFriend removes members from the friends table.\nfunc (r *friendRepository) RemoveFriend(memberId string, userId string) error {\n\treturn r.DB.\n\t\tExec(\"DELETE FROM friends WHERE user_id = ? AND friend_id = ?\", memberId, userId).\n\t\tExec(\"DELETE FROM friends WHERE user_id = ? AND friend_id = ?\", userId, memberId).\n\t\tError\n}\n\n// Save inserts the given user in the DB\nfunc (r *friendRepository) Save(user *model.User) error {\n\treturn r.DB.Save(&user).Error\n}\n"
  },
  {
    "path": "server/repository/guild_repository.go",
    "content": "package repository\n\nimport (\n\t\"errors\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"gorm.io/gorm\"\n\t\"gorm.io/gorm/clause\"\n\t\"log\"\n\t\"time\"\n)\n\n// guildRepository is data/repository implementation\n// of service layer GuildRepository\ntype guildRepository struct {\n\tDB *gorm.DB\n}\n\n// NewGuildRepository is a factory for initializing Guild Repositories\nfunc NewGuildRepository(db *gorm.DB) model.GuildRepository {\n\treturn &guildRepository{\n\t\tDB: db,\n\t}\n}\n\n// List returns all of the given users guilds\nfunc (r *guildRepository) List(uid string) (*[]model.GuildResponse, error) {\n\tvar guilds []model.GuildResponse\n\tresult := r.DB.Raw(`\n\t\tSELECT distinct g.\"id\",\n\t\tg.\"name\",\n\t\tg.\"owner_id\",\n\t\tg.\"icon\",\n\t\tg.\"created_at\",\n\t\tg.\"updated_at\",\n\t\t((SELECT c.\"last_activity\"\n\t\t FROM channels c\n\t\t JOIN guilds g ON g.id = c.\"guild_id\"\n\t\t WHERE g.id = member.\"guild_id\"\n\t\t order by c.\"last_activity\" DESC\n\t\t limit 1) > member.\"last_seen\") AS \"hasNotification\",\n\t\t(SELECT c.id AS \"default_channel_id\"\n\t\tFROM channels c\n\t    JOIN guilds g ON g.id = c.\"guild_id\"\n\t\tWHERE g.id = member.\"guild_id\"\n\t\tORDER BY c.\"created_at\"\n\t\tLIMIT 1)\n\t\tFROM guilds g\n\t\tJOIN members as member\n\t\ton g.\"id\"::text = member.\"guild_id\"\n\t\tWHERE member.\"user_id\" = ?\n\t\tORDER BY g.\"created_at\";\n\t`, uid).Find(&guilds)\n\n\treturn &guilds, result.Error\n}\n\n// GuildMembers returns all members of the given guild and\n// whether they are the given user IDs friend\nfunc (r *guildRepository) GuildMembers(userId string, guildId string) (*[]model.MemberResponse, error) {\n\tvar members []model.MemberResponse\n\tresult := r.DB.Raw(`\n\t\tSELECT u.id,\n\t\tu.username,\n\t\tu.image,\n\t\tu.\"is_online\",\n\t\tu.\"created_at\",\n\t\tu.\"updated_at\",\n\t\tm.nickname,\n\t\tm.color,\n\t\tEXISTS(\n\t\t\tSELECT 1\n\t\t\tFROM users\n\t\t\tLEFT JOIN friends f ON users.id = f.\"user_id\"\n\t\t\tWHERE f.\"friend_id\" = u.id\n\t\t\tAND f.\"user_id\" = ?\n\t\t) AS is_friend\n\t\tFROM users AS u\n\t\tJOIN members m ON u.\"id\"::text = m.\"user_id\"\n\t\tWHERE m.\"guild_id\" = ?\n\t\tORDER BY (CASE WHEN m.nickname notnull THEN m.nickname ELSE u.username END)\n\t`, userId, guildId).Find(&members)\n\n\treturn &members, result.Error\n}\n\n// Create inserts the given guild in the DB\nfunc (r *guildRepository) Create(guild *model.Guild) (*model.Guild, error) {\n\tif result := r.DB.Create(&guild); result.Error != nil {\n\t\tlog.Printf(\"Could not create a guild for user: %v. Reason: %v\\n\", guild.OwnerId, result.Error)\n\t\treturn nil, apperrors.NewInternal()\n\t}\n\n\treturn guild, nil\n}\n\n// FindUserByID returns a user containing all of their guilds\nfunc (r *guildRepository) FindUserByID(uid string) (*model.User, error) {\n\tuser := &model.User{}\n\n\t// we need to actually check errors as it could be something other than not found\n\tif err := r.DB.\n\t\tPreload(\"Guilds\").\n\t\tWhere(\"id = ?\", uid).\n\t\tFirst(&user).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn user, apperrors.NewNotFound(\"uid\", uid)\n\t\t}\n\t\treturn user, apperrors.NewInternal()\n\t}\n\n\treturn user, nil\n}\n\n// FindByID returns the guild for the given id containing all of their fields\nfunc (r *guildRepository) FindByID(id string) (*model.Guild, error) {\n\tguild := &model.Guild{}\n\n\tif err := r.DB.\n\t\tPreload(clause.Associations).\n\t\tWhere(\"id = ?\", id).\n\t\tFirst(&guild).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn guild, apperrors.NewNotFound(\"id\", id)\n\t\t}\n\t\treturn guild, apperrors.NewInternal()\n\t}\n\n\treturn guild, nil\n}\n\n// Save updates the given guild\nfunc (r *guildRepository) Save(guild *model.Guild) error {\n\tif result := r.DB.Save(&guild); result.Error != nil {\n\t\tlog.Printf(\"Could not update the guild with id: %v. Reason: %v\\n\", guild.ID, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\n\treturn nil\n}\n\n// RemoveMember removes the given user from the given guild\nfunc (r *guildRepository) RemoveMember(userId string, guildId string) error {\n\tif result := r.DB.\n\t\tExec(\"DELETE FROM members WHERE user_id = ? AND guild_id = ?\", userId, guildId); result.Error != nil {\n\t\tlog.Printf(\"Could not remove member with id: %s from the guild with id: %v. Reason: %v\\n\", userId, guildId, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\n\treturn nil\n}\n\n// Delete removes the given guild and all its associations\nfunc (r *guildRepository) Delete(guildId string) error {\n\tif result := r.DB.\n\t\tExec(\"DELETE FROM members WHERE guild_id = ?\", guildId).\n\t\tExec(\"DELETE FROM bans WHERE guild_id = ?\", guildId).\n\t\tExec(\"DELETE FROM guilds WHERE id = ?\", guildId); result.Error != nil {\n\t\tlog.Printf(\"Could not delete the guild with id: %v. Reason: %v\\n\", guildId, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\n\treturn nil\n}\n\n// UnbanMember removes the given user from the bans of the given guild\nfunc (r *guildRepository) UnbanMember(userId string, guildId string) error {\n\tif result := r.DB.Exec(\"DELETE FROM bans WHERE guild_id = ? AND user_id = ?\", guildId, userId); result.Error != nil {\n\t\tlog.Printf(\"Could not unban the user with id: %v from the guild with id: %v. Reason: %v\\n\", userId, guildId, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\n// GetBanList returns a list of all banned users from the given guild\nfunc (r *guildRepository) GetBanList(guildId string) (*[]model.BanResponse, error) {\n\tvar bans []model.BanResponse\n\tif result := r.DB.Raw(`\n\t\t\tselect u.id, u.username, u.image\n\t\t\tfrom bans b\n\t\t\tjoin users u on b.\"user_id\" = u.id\n\t\t\twhere b.\"guild_id\" = ?\n\t\t`, guildId).Scan(&bans); result.Error != nil {\n\t\tlog.Printf(\"Could not get the ban list for the guild with id: %v. Reason: %v\\n\", guildId, result.Error)\n\t\treturn &bans, apperrors.NewInternal()\n\t}\n\n\treturn &bans, nil\n}\n\n// GetMemberSettings returns the given members settings in the given guild\nfunc (r *guildRepository) GetMemberSettings(userId string, guildId string) (*model.MemberSettings, error) {\n\tsettings := model.MemberSettings{}\n\terr := r.DB.\n\t\tTable(\"members\").\n\t\tWhere(\"user_id = ? AND guild_id = ?\", userId, guildId).\n\t\tFirst(&settings)\n\treturn &settings, err.Error\n}\n\n// UpdateMemberSettings updates the settings of the given member in the given guild\nfunc (r *guildRepository) UpdateMemberSettings(settings *model.MemberSettings, userId string, guildId string) error {\n\terr := r.DB.\n\t\tTable(\"members\").\n\t\tWhere(\"user_id = ? AND guild_id = ?\", userId, guildId).\n\t\tUpdates(map[string]any{\n\t\t\t\"color\":      settings.Color,\n\t\t\t\"nickname\":   settings.Nickname,\n\t\t\t\"updated_at\": time.Now(),\n\t\t}).\n\t\tError\n\treturn err\n}\n\n// FindUsersByIds returns the found users for the given user IDs and guild ID\nfunc (r *guildRepository) FindUsersByIds(ids []string, guildId string) (*[]model.User, error) {\n\tvar users []model.User\n\tresult := r.DB.Raw(`\n\t\tSELECT u.*\n\t\tFROM users AS u\n\t\tJOIN members m ON u.\"id\"::text = m.\"user_id\"\n\t\tWHERE m.\"guild_id\" = ?\n\t\tAND m.\"user_id\" IN ?\n\t`, guildId, ids).Find(&users)\n\n\treturn &users, result.Error\n}\n\n// GetMember returns user for the given userId and guildId\nfunc (r *guildRepository) GetMember(userId, guildId string) (*model.User, error) {\n\tvar user model.User\n\tresult := r.DB.Raw(`\n\t\tSELECT u.*\n\t\tFROM users AS u\n\t\tJOIN members m ON u.\"id\"::text = m.\"user_id\"\n\t\tWHERE m.\"guild_id\" = ?\n\t\tAND m.\"user_id\" = ?\n\t`, guildId, userId).Find(&user)\n\n\treturn &user, result.Error\n}\n\n// UpdateMemberLastSeen sets the LastSeen field of the given user to the current date\nfunc (r *guildRepository) UpdateMemberLastSeen(userId, guildId string) error {\n\terr := r.DB.\n\t\tTable(\"members\").\n\t\tWhere(\"user_id = ? AND guild_id = ?\", userId, guildId).\n\t\tUpdates(map[string]any{\n\t\t\t\"last_seen\": time.Now(),\n\t\t}).\n\t\tError\n\treturn err\n}\n\n// GetMemberIds returns the ids of all members of the given guild\nfunc (r *guildRepository) GetMemberIds(guildId string) (*[]string, error) {\n\tvar users []string\n\tresult := r.DB.Raw(`\n\t\tSELECT u.id\n\t\tFROM users AS u\n\t\tJOIN members m ON u.\"id\"::text = m.\"user_id\"\n\t\tWHERE m.\"guild_id\" = ?\n\t`, guildId).Find(&users)\n\n\treturn &users, result.Error\n}\n\nfunc (r *guildRepository) RemoveVCMember(userId, guildId string) error {\n\tif result := r.DB.Exec(\"DELETE FROM vc_members WHERE guild_id = ? AND user_id = ?\", guildId, userId); result.Error != nil {\n\t\tlog.Printf(\"Could not add the user with id: %v to the vc of the guild with id: %v. Reason: %v\\n\", userId, guildId, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\nfunc (r *guildRepository) VCMembers(guildId string) (*[]model.VCMemberResponse, error) {\n\tvar members []model.VCMemberResponse\n\tresult := r.DB.Raw(`\n\t\tSELECT u.id,\n\t\t\t   u.username,\n\t\t\t   u.image,\n\t\t\t   vm.is_muted,\n\t\t\t   vm.is_deafened,\n\t\t\t   m.nickname\n\t\tFROM users AS u\n\t\t\t\t JOIN vc_members vm ON u.\"id\"::text = vm.\"user_id\"\n\t\t\t\t JOIN members m on vm.user_id = m.user_id AND vm.guild_id = m.guild_id\n\t\tWHERE vm.\"guild_id\" = ?\n\t\tORDER BY (CASE WHEN m.nickname notnull THEN m.nickname ELSE u.username END)\n\t`, guildId).Find(&members)\n\n\treturn &members, result.Error\n}\n\nfunc (r *guildRepository) UpdateVCMember(isMuted, isDeafened bool, userId, guildId string) error {\n\terr := r.DB.\n\t\tTable(\"vc_members\").\n\t\tWhere(\"user_id = ? AND guild_id = ?\", userId, guildId).\n\t\tUpdates(map[string]any{\n\t\t\t\"is_muted\":    isMuted,\n\t\t\t\"is_deafened\": isDeafened,\n\t\t}).\n\t\tError\n\treturn err\n}\n\nfunc (r *guildRepository) GetVCMember(userId, guildId string) (*model.VCMember, error) {\n\tvar user model.VCMember\n\n\tresult := r.DB.Raw(`\n\t\tSELECT * from vc_members vm\n\t\tWHERE vm.\"guild_id\" = ?\n\t\tAND vm.\"user_id\" = ?\n\t`, guildId, userId).Find(&user)\n\n\treturn &user, result.Error\n}\n"
  },
  {
    "path": "server/repository/mail_repository.go",
    "content": "package repository\n\nimport (\n\t\"fmt\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"net/smtp\"\n)\n\n// mailRepository contains the gmail username and password\n// as well as the frontend origin.\ntype mailRepository struct {\n\tusername string\n\tpassword string\n\torigin   string\n}\n\n// NewMailRepository is a factory for initializing Mail Repositories\nfunc NewMailRepository(username string, password string, origin string) model.MailRepository {\n\treturn &mailRepository{\n\t\tusername: username,\n\t\tpassword: password,\n\t\torigin:   origin,\n\t}\n}\n\n// SendResetMail sends a password reset email with the given reset token\nfunc (m *mailRepository) SendResetMail(email string, token string) error {\n\n\tmsg := \"From: \" + m.username + \"\\n\" +\n\t\t\"To: \" + email + \"\\n\" +\n\t\t\"Subject: Reset Email\\n\\n\" +\n\t\tfmt.Sprintf(\"<a href=\\\"%s/reset-password/%s\\\">Reset Password</a>\", m.origin, token)\n\n\terr := smtp.SendMail(\"smtp.gmail.com:587\",\n\t\tsmtp.PlainAuth(\"\", m.username, m.password, \"smtp.gmail.com\"),\n\t\tm.username, []string{email}, []byte(msg))\n\n\treturn err\n}\n"
  },
  {
    "path": "server/repository/message_repository.go",
    "content": "package repository\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"fmt\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"gorm.io/gorm\"\n\t\"log\"\n\t\"time\"\n)\n\n// messageRepository is data/repository implementation\n// of service layer MessageRepository\ntype messageRepository struct {\n\tDB *gorm.DB\n}\n\n// NewMessageRepository is a factory for initializing Message Repositories\nfunc NewMessageRepository(db *gorm.DB) model.MessageRepository {\n\treturn &messageRepository{\n\t\tDB: db,\n\t}\n}\n\n// messageQuery represents the fetched fields for GetMessages\ntype messageQuery struct {\n\tId            string\n\tText          *string\n\tCreatedAt     time.Time\n\tUpdatedAt     time.Time\n\tFileType      *string\n\tUrl           *string\n\tFilename      *string\n\tAttachmentId  *string\n\tUserId        string\n\tUserCreatedAt time.Time\n\tUserUpdatedAt time.Time\n\tUsername      string\n\tImage         string\n\tIsOnline      bool\n\tNickname      *string\n\tColor         *string\n\tIsFriend      bool\n}\n\n// GetMessages returns the 35 most recent messages for the given channel.\n// If a cursor is specified it returns the 35 messages after the cursor.\nfunc (r *messageRepository) GetMessages(userId string, channel *model.Channel, cursor string) (*[]model.MessageResponse, error) {\n\tvar result []messageQuery\n\n\tmemberSelect := \"\"\n\tmemberJoin := \"\"\n\tmemberWhere := \"\"\n\n\t// If the channel is not a DM channel, also fetch the message author's settings\n\tif !channel.IsDM {\n\t\tmemberSelect = \"member.nickname, member.color,\"\n\t\tmemberJoin = \"LEFT JOIN members member on messages.user_id = member.user_id\"\n\t\tmemberWhere = fmt.Sprintf(\"AND member.guild_id = %s::text\", *channel.GuildID)\n\t}\n\n\tcrs := \"\"\n\tif cursor != \"\" {\n\t\t// Remove the timezone from the string since it's stored differently in the DB\n\t\tdate := cursor[:len(cursor)-6]\n\t\tcrs = fmt.Sprintf(\"AND messages.created_at < '%s'\", date)\n\t}\n\n\terr := r.DB.\n\t\tRaw(fmt.Sprintf(`\n\t\tSELECT messages.id,\n\t\t\tmessages.text,\n\t\t\tmessages.created_at,\n\t\t\tmessages.updated_at,\n\t\t\ta.file_type,\n\t\t\ta.url,\n\t\t\ta.filename,\n\t\t\ta.id                as \"attachment_id\",\n\t\t\tusers.id         as \"user_id\",\n\t\t\tusers.created_at as \"user_created_at\",\n\t\t\tusers.updated_at as \"user_updated_at\",\n\t\t\tusers.username,\n\t\t\tusers.image,\n\t\t\tusers.is_online,\n\t\t\t%s \n\t\t\tEXISTS(\n\t\t\t  SELECT 1\n\t\t\t  FROM users\n\t\t\t   LEFT JOIN friends f ON users.id = f.user_id\n\t\t\t  WHERE f.friend_id = messages.user_id\n\t\t\t\tAND f.user_id = @userId) as is_friend\n\t\tFROM messages\n\t\tLEFT JOIN \"users\"\n\t\tON users.id = messages.user_id\n\t\tLEFT JOIN attachments a\n\t\tON a.message_id = messages.id\n\t\t%s\n\t\tWHERE messages.channel_id = @channelId\n\t\t%s \n\t\t%s \n\t\tORDER BY messages.created_at DESC\n\t\tLIMIT 35\n`, memberSelect, memberJoin, memberWhere, crs),\n\t\t\tsql.Named(\"userId\", userId),\n\t\t\tsql.Named(\"channelId\", channel.ID)).\n\t\tScan(&result).Error\n\n\tvar messages []model.MessageResponse\n\n\t// Turn messageQuery results into MessageResponse\n\tfor _, m := range result {\n\n\t\tvar attachment *model.Attachment = nil\n\t\tif m.AttachmentId != nil {\n\t\t\tattachment = &model.Attachment{\n\t\t\t\tUrl:      *m.Url,\n\t\t\t\tFileType: *m.FileType,\n\t\t\t\tFilename: *m.Filename,\n\t\t\t}\n\t\t}\n\n\t\tmessage := model.MessageResponse{\n\t\t\tId:         m.Id,\n\t\t\tText:       m.Text,\n\t\t\tCreatedAt:  m.CreatedAt,\n\t\t\tUpdatedAt:  m.UpdatedAt,\n\t\t\tAttachment: attachment,\n\t\t\tUser: model.MemberResponse{\n\t\t\t\tId:        m.UserId,\n\t\t\t\tUsername:  m.Username,\n\t\t\t\tImage:     m.Image,\n\t\t\t\tIsOnline:  m.IsOnline,\n\t\t\t\tCreatedAt: m.UserCreatedAt,\n\t\t\t\tUpdatedAt: m.UserUpdatedAt,\n\t\t\t\tNickname:  m.Nickname,\n\t\t\t\tColor:     m.Color,\n\t\t\t\tIsFriend:  m.IsFriend,\n\t\t\t},\n\t\t}\n\t\tmessages = append(messages, message)\n\t}\n\n\treturn &messages, err\n}\n\n// CreateMessage inserts the message in the DB\nfunc (r *messageRepository) CreateMessage(message *model.Message) (*model.Message, error) {\n\tif result := r.DB.Create(&message); result.Error != nil {\n\t\tlog.Printf(\"Could not create a message for user: %v. Reason: %v\\n\", message.UserId, result.Error)\n\t\treturn nil, apperrors.NewInternal()\n\t}\n\n\treturn message, nil\n}\n\n// UpdateMessage updates the message in the DB\nfunc (r *messageRepository) UpdateMessage(message *model.Message) error {\n\tif result := r.DB.Save(&message); result.Error != nil {\n\t\tlog.Printf(\"Could not update message with id: %v. Reason: %v\\n\", message.ID, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\n// DeleteMessage removes the message from the DB\nfunc (r *messageRepository) DeleteMessage(message *model.Message) error {\n\tif result := r.DB.Delete(message); result.Error != nil {\n\t\tlog.Printf(\"Could not delete message with id: %v. Reason: %v\\n\", message.ID, result.Error)\n\t\treturn apperrors.NewInternal()\n\t}\n\treturn nil\n}\n\n// GetById fetches the message for the given id\nfunc (r *messageRepository) GetById(messageId string) (*model.Message, error) {\n\tmessage := &model.Message{}\n\n\tif result := r.DB.Where(\"id = ?\", messageId).First(message); result.Error != nil {\n\t\tif errors.Is(result.Error, gorm.ErrRecordNotFound) {\n\t\t\treturn message, apperrors.NewNotFound(\"message\", messageId)\n\t\t}\n\t\treturn message, apperrors.NewInternal()\n\t}\n\n\treturn message, nil\n}\n"
  },
  {
    "path": "server/repository/redis_repository.go",
    "content": "package repository\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\tgonanoid \"github.com/matoous/go-nanoid/v2\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"time\"\n)\n\ntype redisRepository struct {\n\trds *redis.Client\n}\n\n// NewRedisRepository is a factory for initializing Redis Repositories\nfunc NewRedisRepository(rds *redis.Client) model.RedisRepository {\n\treturn &redisRepository{\n\t\trds: rds,\n\t}\n}\n\n// Redis Prefixes\nconst (\n\tInviteLinkPrefix     = \"inviteLink\"\n\tForgotPasswordPrefix = \"forgot-password\"\n)\n\n// SetResetToken inserts a password reset token in the DB and returns the generated token\nfunc (r *redisRepository) SetResetToken(ctx context.Context, id string) (string, error) {\n\tuid, err := gonanoid.New()\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to generate id: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tif err = r.rds.Set(ctx, fmt.Sprintf(\"%s:%s\", ForgotPasswordPrefix, uid), id, 24*time.Hour).Err(); err != nil {\n\t\tlog.Printf(\"Failed to set link in redis: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\treturn uid, nil\n}\n\n// GetIdFromToken returns the user ID from the DB for the given token\nfunc (r *redisRepository) GetIdFromToken(ctx context.Context, token string) (string, error) {\n\tkey := fmt.Sprintf(\"%s:%s\", ForgotPasswordPrefix, token)\n\tval, err := r.rds.Get(ctx, key).Result()\n\n\tif err == redis.Nil {\n\t\treturn \"\", apperrors.NewBadRequest(apperrors.InvalidResetToken)\n\t}\n\tif err != nil {\n\t\tlog.Printf(\"Failed to get value from redis: %v\\n\", err)\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tr.rds.Del(ctx, key)\n\n\treturn val, nil\n}\n\n// SaveInvite inserts an invite for the given guild in the DB.\n// If isPermanent is true, the invite won't expire\nfunc (r *redisRepository) SaveInvite(ctx context.Context, guildId string, id string, isPermanent bool) error {\n\n\tinvite := model.Invite{GuildId: guildId, IsPermanent: isPermanent}\n\n\tvalue, err := json.Marshal(invite)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error marshalling: %v\\n\", err.Error())\n\t\treturn apperrors.NewInternal()\n\t}\n\n\texpiration := 24 * time.Hour\n\tif isPermanent {\n\t\texpiration = 0\n\t}\n\n\tif result := r.rds.Set(ctx, fmt.Sprintf(\"%s:%s\", InviteLinkPrefix, id), value, expiration); result.Err() != nil {\n\t\tlog.Printf(\"Failed to set invite link in redis: %v\\n\", err.Error())\n\t\treturn apperrors.NewInternal()\n\t}\n\n\treturn nil\n}\n\n// GetInvite returns the stored guild Id for the given token.\nfunc (r *redisRepository) GetInvite(ctx context.Context, token string) (string, error) {\n\tkey := fmt.Sprintf(\"%s:%s\", InviteLinkPrefix, token)\n\tval, err := r.rds.Get(ctx, key).Result()\n\n\tif err != nil {\n\t\tlog.Printf(\"Failed to get invite link from redis: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tvar invite model.Invite\n\terr = json.Unmarshal([]byte(val), &invite)\n\n\tif err != nil {\n\t\tlog.Printf(\"Error unmarshalling: %v\\n\", err.Error())\n\t\treturn \"\", apperrors.NewInternal()\n\t}\n\n\tif !invite.IsPermanent {\n\t\tr.rds.Del(ctx, key)\n\t}\n\n\treturn invite.GuildId, nil\n}\n\n// InvalidateInvites deletes all permanent invites in the DB for the given guild\nfunc (r *redisRepository) InvalidateInvites(ctx context.Context, guild *model.Guild) {\n\tfor _, v := range guild.InviteLinks {\n\t\tkey := fmt.Sprintf(\"%s:%s\", InviteLinkPrefix, v)\n\t\tr.rds.Del(ctx, key)\n\t}\n}\n"
  },
  {
    "path": "server/repository/user_repository.go",
    "content": "package repository\n\nimport (\n\t\"database/sql\"\n\t\"errors\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"gorm.io/gorm\"\n\t\"log\"\n\t\"regexp\"\n)\n\n// userRepository is data/repository implementation\n// of service layer UserRepository\ntype userRepository struct {\n\tDB *gorm.DB\n}\n\n// NewUserRepository is a factory for initializing User Repositories\nfunc NewUserRepository(db *gorm.DB) model.UserRepository {\n\treturn &userRepository{\n\t\tDB: db,\n\t}\n}\n\n// FindByID returns a user for the given ID\nfunc (r *userRepository) FindByID(id string) (*model.User, error) {\n\tuser := &model.User{}\n\n\t// we need to actually check errors as it could be something other than not found\n\tif err := r.DB.Where(\"id = ?\", id).First(&user).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn user, apperrors.NewNotFound(\"uid\", id)\n\t\t}\n\t\treturn user, apperrors.NewInternal()\n\t}\n\n\treturn user, nil\n}\n\n// Create inserts the user in the DB\nfunc (r *userRepository) Create(user *model.User) (*model.User, error) {\n\tif result := r.DB.Create(&user); result.Error != nil {\n\t\t// check unique constraint\n\t\tif isDuplicateKeyError(result.Error) {\n\t\t\treturn nil, apperrors.NewBadRequest(apperrors.DuplicateEmail)\n\t\t}\n\n\t\tlog.Printf(\"Could not create a user with email: %v. Reason: %v\\n\", user.Email, result.Error)\n\t\treturn nil, apperrors.NewInternal()\n\t}\n\n\treturn user, nil\n}\n\n// FindByEmail retrieves user row by email address\nfunc (r *userRepository) FindByEmail(email string) (*model.User, error) {\n\tuser := &model.User{}\n\n\t// we need to actually check errors as it could be something other than not found\n\tif err := r.DB.Where(\"email = ?\", email).First(&user).Error; err != nil {\n\t\tif errors.Is(err, gorm.ErrRecordNotFound) {\n\t\t\treturn user, apperrors.NewNotFound(\"email\", email)\n\t\t}\n\t\treturn user, apperrors.NewInternal()\n\t}\n\n\treturn user, nil\n}\n\n// Update updates the user in the DB\nfunc (r *userRepository) Update(user *model.User) error {\n\treturn r.DB.Save(&user).Error\n}\n\n// GetFriendAndGuildIds returns the id of the users friends and the guilds they are part of\nfunc (r *userRepository) GetFriendAndGuildIds(userId string) (*[]string, error) {\n\tvar ids []string\n\tresult := r.DB.Raw(`\n          SELECT g.id\n          FROM guilds g\n          JOIN members m on m.guild_id = g.\"id\"\n          where m.user_id = @userId\n          UNION\n          SELECT \"User__friends\".\"id\"\n          FROM \"users\" \"User\" LEFT JOIN \"friends\" \"User_User__friends\" ON \"User_User__friends\".\"user_id\"=\"User\".\"id\" LEFT\n              JOIN \"users\" \"User__friends\" ON \"User__friends\".\"id\"=\"User_User__friends\".\"friend_id\"\n          WHERE ( \"User\".\"id\" = @userId )\n\t`, sql.Named(\"userId\", userId)).Find(&ids)\n\n\treturn &ids, result.Error\n}\n\n// GetRequestCount returns the amount of incoming friend requests the current user currently has\nfunc (r *userRepository) GetRequestCount(userId string) (*int64, error) {\n\tvar count int64\n\terr := r.DB.\n\t\tTable(\"users\").\n\t\tJoins(\"JOIN friend_requests fr ON users.id = fr.sender_id\").\n\t\tWhere(\"fr.receiver_id = ?\", userId).\n\t\tCount(&count).\n\t\tError\n\n\treturn &count, err\n}\n\n// isDuplicateKeyError checks if the provided error is a PostgreSQL duplicate key error\nfunc isDuplicateKeyError(err error) bool {\n\tduplicate := regexp.MustCompile(`\\(SQLSTATE 23505\\)$`)\n\treturn duplicate.MatchString(err.Error())\n}\n"
  },
  {
    "path": "server/service/channel_service.go",
    "content": "package service\n\nimport (\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n)\n\n// channelService acts as a struct for injecting an implementation of ChannelRepository\n// for use in service methods\ntype channelService struct {\n\tChannelRepository model.ChannelRepository\n\tGuildRepository   model.GuildRepository\n}\n\n// CSConfig will hold repositories that will eventually be injected into\n// this service layer\ntype CSConfig struct {\n\tChannelRepository model.ChannelRepository\n\tGuildRepository   model.GuildRepository\n}\n\n// NewChannelService is a factory function for\n// initializing a ChannelService with its repository layer dependencies\nfunc NewChannelService(c *CSConfig) model.ChannelService {\n\treturn &channelService{\n\t\tChannelRepository: c.ChannelRepository,\n\t\tGuildRepository:   c.GuildRepository,\n\t}\n}\n\nfunc (c *channelService) CreateChannel(channel *model.Channel) (*model.Channel, error) {\n\tchannel.ID = GenerateId()\n\n\treturn c.ChannelRepository.Create(channel)\n}\n\nfunc (c *channelService) GetChannels(userId string, guildId string) (*[]model.ChannelResponse, error) {\n\treturn c.ChannelRepository.Get(userId, guildId)\n}\n\nfunc (c *channelService) Get(channelId string) (*model.Channel, error) {\n\treturn c.ChannelRepository.GetById(channelId)\n}\n\nfunc (c *channelService) GetPrivateChannelMembers(channelId string) (*[]string, error) {\n\treturn c.ChannelRepository.GetPrivateChannelMembers(channelId)\n}\n\nfunc (c *channelService) GetDirectMessages(userId string) (*[]model.DirectMessage, error) {\n\treturn c.ChannelRepository.GetDirectMessages(userId)\n}\n\nfunc (c *channelService) GetDirectMessageChannel(userId string, memberId string) (*string, error) {\n\treturn c.ChannelRepository.GetDirectMessageChannel(userId, memberId)\n}\n\nfunc (c *channelService) AddDMChannelMembers(memberIds []string, channelId string, userId string) error {\n\tvar members []model.DMMember\n\tfor _, mId := range memberIds {\n\t\tmember := model.DMMember{\n\t\t\tID:        GenerateId(),\n\t\t\tUserID:    mId,\n\t\t\tChannelId: channelId,\n\t\t\tIsOpen:    userId == mId,\n\t\t}\n\t\tmembers = append(members, member)\n\t}\n\n\treturn c.ChannelRepository.AddDMChannelMembers(members)\n}\n\nfunc (c *channelService) SetDirectMessageStatus(dmId string, userId string, isOpen bool) error {\n\treturn c.ChannelRepository.SetDirectMessageStatus(dmId, userId, isOpen)\n}\n\nfunc (c *channelService) DeleteChannel(channel *model.Channel) error {\n\treturn c.ChannelRepository.DeleteChannel(channel)\n}\n\nfunc (c *channelService) UpdateChannel(channel *model.Channel) error {\n\treturn c.ChannelRepository.UpdateChannel(channel)\n}\n\nfunc (c *channelService) CleanPCMembers(channelId string) error {\n\treturn c.ChannelRepository.CleanPCMembers(channelId)\n}\n\nfunc (c *channelService) AddPrivateChannelMembers(memberIds []string, channelId string) error {\n\treturn c.ChannelRepository.AddPrivateChannelMembers(memberIds, channelId)\n}\n\nfunc (c *channelService) RemovePrivateChannelMembers(memberIds []string, channelId string) error {\n\treturn c.ChannelRepository.RemovePrivateChannelMembers(memberIds, channelId)\n}\n\nfunc (c *channelService) OpenDMForAll(dmId string) error {\n\treturn c.ChannelRepository.OpenDMForAll(dmId)\n}\n\nfunc (c *channelService) GetDMByUserAndChannel(userId string, channelId string) (string, error) {\n\treturn c.ChannelRepository.FindDMByUserAndChannelId(channelId, userId)\n}\n\n// IsChannelMember checks if the user has access to the given channel.\n// Returns an error if they do not, otherwise nil\nfunc (c *channelService) IsChannelMember(channel *model.Channel, userId string) error {\n\t// Check if user has access to the channel if it's private\n\tif !channel.IsPublic {\n\t\t// Channel is DM -> Check if one of the members\n\t\tif channel.IsDM {\n\t\t\tid, err := c.ChannelRepository.FindDMByUserAndChannelId(channel.ID, userId)\n\n\t\t\tif err != nil || id == \"\" {\n\t\t\t\treturn apperrors.NewAuthorization(apperrors.Unauthorized)\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t\t// Channel is private\n\t\tfor _, member := range channel.PCMembers {\n\t\t\tif member.ID == userId {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t\treturn apperrors.NewAuthorization(apperrors.Unauthorized)\n\t}\n\n\t// Check if user has access to the channel\n\tmember, err := c.GuildRepository.GetMember(userId, *channel.GuildID)\n\tif err != nil || member.ID == \"\" {\n\t\treturn apperrors.NewAuthorization(apperrors.Unauthorized)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/service/channel_service_test.go",
    "content": "package service\n\nimport (\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"testing\"\n)\n\nfunc TestChannelService_CreateChannel(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\t\tmockChannel := fixture.GetMockChannel(\"\")\n\n\t\tparams := &model.Channel{\n\t\t\tGuildID:  mockChannel.GuildID,\n\t\t\tName:     mockChannel.Name,\n\t\t\tIsPublic: true,\n\t\t}\n\n\t\tmockChannelRepository := new(mocks.ChannelRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tChannelRepository: mockChannelRepository,\n\t\t})\n\n\t\tmockChannelRepository.\n\t\t\tOn(\"Create\", params).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockChannel.ID = uid\n\t\t\t}).Return(mockChannel, nil)\n\n\t\tchannel, err := cs.CreateChannel(params)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, uid, mockChannel.ID)\n\t\tassert.Equal(t, channel, mockChannel)\n\n\t\tmockChannelRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(\"\")\n\n\t\tparams := &model.Channel{\n\t\t\tGuildID:  mockChannel.GuildID,\n\t\t\tName:     mockChannel.Name,\n\t\t\tIsPublic: true,\n\t\t}\n\n\t\tmockChannelRepository := new(mocks.ChannelRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tChannelRepository: mockChannelRepository,\n\t\t})\n\n\t\tmockErr := apperrors.NewInternal()\n\n\t\tmockChannelRepository.\n\t\t\tOn(\"Create\", params).\n\t\t\tReturn(nil, mockErr)\n\n\t\tchannel, err := cs.CreateChannel(params)\n\n\t\t// assert error is error we response with in mock\n\t\tassert.EqualError(t, err, mockErr.Error())\n\t\tassert.Nil(t, channel)\n\n\t\tmockChannelRepository.AssertExpectations(t)\n\t})\n}\n\nfunc TestChannelService_AddDMChannelMembers(t *testing.T) {\n\tuserId := fixture.RandID()\n\tids := []string{userId, fixture.RandID()}\n\tchannelId := fixture.RandID()\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockChannelRepository := new(mocks.ChannelRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tChannelRepository: mockChannelRepository,\n\t\t})\n\n\t\tmockChannelRepository.\n\t\t\tOn(\"AddDMChannelMembers\", mock.AnythingOfType(\"[]model.DMMember\")).\n\t\t\t// Has to be added so the different IDs get accepted\n\t\t\tReturn(nil)\n\n\t\terr := cs.AddDMChannelMembers(ids, channelId, userId)\n\t\tassert.NoError(t, err)\n\n\t\tmockChannelRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockChannelRepository := new(mocks.ChannelRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tChannelRepository: mockChannelRepository,\n\t\t})\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockChannelRepository.\n\t\t\tOn(\"AddDMChannelMembers\", mock.AnythingOfType(\"[]model.DMMember\")).\n\t\t\t// Has to be added so the different IDs get accepted\n\t\t\tReturn(mockError)\n\n\t\terr := cs.AddDMChannelMembers(ids, channelId, userId)\n\t\tassert.Error(t, err)\n\n\t\tmockChannelRepository.AssertExpectations(t)\n\t})\n}\n\nfunc TestChannelService_IsChannelMember(t *testing.T) {\n\tmockUser := fixture.GetMockUser()\n\n\tt.Run(\"User is member of the DM\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockDMChannel()\n\n\t\tmockChannelRepository := new(mocks.ChannelRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tChannelRepository: mockChannelRepository,\n\t\t})\n\n\t\tmockChannelRepository.On(\"FindDMByUserAndChannelId\", mockChannel.ID, mockUser.ID).Return(fixture.RandID(), nil)\n\n\t\terr := cs.IsChannelMember(mockChannel, mockUser.ID)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"User is not member of the DM\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockDMChannel()\n\n\t\tmockChannelRepository := new(mocks.ChannelRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tChannelRepository: mockChannelRepository,\n\t\t})\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.Unauthorized)\n\t\tmockChannelRepository.On(\"FindDMByUserAndChannelId\", mockChannel.ID, mockUser.ID).Return(\"\", mockError)\n\n\t\terr := cs.IsChannelMember(mockChannel, mockUser.ID)\n\t\tassert.Error(t, err)\n\t\tassert.Equal(t, err, mockError)\n\t})\n\n\tt.Run(\"User is member of the private channel\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(\"\")\n\t\tmockChannel.IsPublic = false\n\t\tmockChannel.PCMembers = append(mockChannel.PCMembers, *mockUser)\n\n\t\tmockChannelRepository := new(mocks.ChannelRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tChannelRepository: mockChannelRepository,\n\t\t})\n\n\t\terr := cs.IsChannelMember(mockChannel, mockUser.ID)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"User is not member of the private channel\", func(t *testing.T) {\n\t\tmockChannel := fixture.GetMockChannel(\"\")\n\t\tmockChannel.IsPublic = false\n\n\t\tmockChannelRepository := new(mocks.ChannelRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tChannelRepository: mockChannelRepository,\n\t\t})\n\n\t\terr := cs.IsChannelMember(mockChannel, mockUser.ID)\n\t\tassert.Error(t, err)\n\t\tassert.Equal(t, err, apperrors.NewAuthorization(apperrors.Unauthorized))\n\t})\n\n\tt.Run(\"User is a guild member\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockGuild.Members = append(mockGuild.Members, *mockUser)\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildRepository := new(mocks.GuildRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tGuildRepository: mockGuildRepository,\n\t\t})\n\n\t\tmockGuildRepository.On(\"GetMember\", mockUser.ID, *mockChannel.GuildID).Return(mockUser, nil)\n\n\t\terr := cs.IsChannelMember(mockChannel, mockUser.ID)\n\t\tassert.NoError(t, err)\n\t})\n\n\tt.Run(\"User is not a member of the guild\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\t\tmockChannel := fixture.GetMockChannel(mockGuild.ID)\n\n\t\tmockGuildRepository := new(mocks.GuildRepository)\n\t\tcs := NewChannelService(&CSConfig{\n\t\t\tGuildRepository: mockGuildRepository,\n\t\t})\n\n\t\tmockError := apperrors.NewAuthorization(apperrors.Unauthorized)\n\t\tmockGuildRepository.On(\"GetMember\", mockUser.ID, *mockChannel.GuildID).Return(nil, mockError)\n\n\t\terr := cs.IsChannelMember(mockChannel, mockUser.ID)\n\t\tassert.Error(t, err)\n\t\tassert.Equal(t, err, mockError)\n\t})\n}\n"
  },
  {
    "path": "server/service/friend_service.go",
    "content": "package service\n\nimport (\n\t\"github.com/sentrionic/valkyrie/model\"\n)\n\n// friendService acts as a struct for injecting an implementation of UserRepository\n// and FriendRepository for use in service methods\ntype friendService struct {\n\tUserRepository   model.UserRepository\n\tFriendRepository model.FriendRepository\n}\n\n// FSConfig will hold repositories that will eventually be injected into\n// this service layer\ntype FSConfig struct {\n\tUserRepository   model.UserRepository\n\tFriendRepository model.FriendRepository\n}\n\n// NewFriendService is a factory function for\n// initializing a FriendService with its repository layer dependencies\nfunc NewFriendService(c *FSConfig) model.FriendService {\n\treturn &friendService{\n\t\tUserRepository:   c.UserRepository,\n\t\tFriendRepository: c.FriendRepository,\n\t}\n}\n\nfunc (f *friendService) GetFriends(id string) (*[]model.Friend, error) {\n\treturn f.FriendRepository.FriendsList(id)\n}\n\nfunc (f *friendService) GetRequests(id string) (*[]model.FriendRequest, error) {\n\treturn f.FriendRepository.RequestList(id)\n}\n\nfunc (f *friendService) GetMemberById(id string) (*model.User, error) {\n\treturn f.FriendRepository.FindByID(id)\n}\n\nfunc (f *friendService) DeleteRequest(memberId string, userId string) error {\n\treturn f.FriendRepository.DeleteRequest(memberId, userId)\n}\n\nfunc (f *friendService) RemoveFriend(memberId string, userId string) error {\n\treturn f.FriendRepository.RemoveFriend(memberId, userId)\n}\n\nfunc (f *friendService) SaveRequests(user *model.User) error {\n\treturn f.FriendRepository.Save(user)\n}\n"
  },
  {
    "path": "server/service/guild_service.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\tgonanoid \"github.com/matoous/go-nanoid\"\n\t\"github.com/sentrionic/valkyrie/model\"\n)\n\n// GuildService acts as a struct for injecting an implementation of GuildRepository\n// for use in service methods\ntype guildService struct {\n\tUserRepository    model.UserRepository\n\tFileRepository    model.FileRepository\n\tRedisRepository   model.RedisRepository\n\tGuildRepository   model.GuildRepository\n\tChannelRepository model.ChannelRepository\n}\n\n// GSConfig will hold repositories that will eventually be injected into\n// this service layer\ntype GSConfig struct {\n\tUserRepository    model.UserRepository\n\tFileRepository    model.FileRepository\n\tRedisRepository   model.RedisRepository\n\tGuildRepository   model.GuildRepository\n\tChannelRepository model.ChannelRepository\n}\n\n// NewGuildService is a factory function for\n// initializing a GuildService with its repository layer dependencies\nfunc NewGuildService(c *GSConfig) model.GuildService {\n\treturn &guildService{\n\t\tUserRepository:    c.UserRepository,\n\t\tFileRepository:    c.FileRepository,\n\t\tRedisRepository:   c.RedisRepository,\n\t\tGuildRepository:   c.GuildRepository,\n\t\tChannelRepository: c.ChannelRepository,\n\t}\n}\n\nfunc (g *guildService) GetUserGuilds(uid string) (*[]model.GuildResponse, error) {\n\treturn g.GuildRepository.List(uid)\n}\n\nfunc (g *guildService) GetGuildMembers(userId string, guildId string) (*[]model.MemberResponse, error) {\n\treturn g.GuildRepository.GuildMembers(userId, guildId)\n}\n\nfunc (g *guildService) CreateGuild(guild *model.Guild) (*model.Guild, error) {\n\tguild.ID = GenerateId()\n\n\treturn g.GuildRepository.Create(guild)\n}\n\nfunc (g *guildService) GetUser(uid string) (*model.User, error) {\n\treturn g.GuildRepository.FindUserByID(uid)\n}\n\nfunc (g *guildService) GetGuild(id string) (*model.Guild, error) {\n\treturn g.GuildRepository.FindByID(id)\n}\n\nfunc (g *guildService) GenerateInviteLink(ctx context.Context, guildId string, isPermanent bool) (string, error) {\n\tid, err := gonanoid.Nanoid(8)\n\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err = g.RedisRepository.SaveInvite(ctx, guildId, id, isPermanent); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn id, nil\n}\n\nfunc (g *guildService) UpdateGuild(guild *model.Guild) error {\n\treturn g.GuildRepository.Save(guild)\n}\n\nfunc (g *guildService) GetGuildIdFromInvite(ctx context.Context, token string) (string, error) {\n\treturn g.RedisRepository.GetInvite(ctx, token)\n}\n\nfunc (g *guildService) GetDefaultChannel(guildId string) (*model.Channel, error) {\n\treturn g.ChannelRepository.GetGuildDefault(guildId)\n}\n\nfunc (g *guildService) InvalidateInvites(ctx context.Context, guild *model.Guild) {\n\tg.RedisRepository.InvalidateInvites(ctx, guild)\n}\n\nfunc (g *guildService) RemoveMember(userId string, guildId string) error {\n\treturn g.GuildRepository.RemoveMember(userId, guildId)\n}\n\nfunc (g *guildService) DeleteGuild(guildId string) error {\n\treturn g.GuildRepository.Delete(guildId)\n}\n\nfunc (g *guildService) UnbanMember(userId string, guildId string) error {\n\treturn g.GuildRepository.UnbanMember(userId, guildId)\n}\n\nfunc (g *guildService) GetBanList(guildId string) (*[]model.BanResponse, error) {\n\treturn g.GuildRepository.GetBanList(guildId)\n}\n\nfunc (g *guildService) GetMemberSettings(userId string, guildId string) (*model.MemberSettings, error) {\n\treturn g.GuildRepository.GetMemberSettings(userId, guildId)\n}\n\nfunc (g *guildService) UpdateMemberSettings(settings *model.MemberSettings, userId string, guildId string) error {\n\treturn g.GuildRepository.UpdateMemberSettings(settings, userId, guildId)\n}\n\nfunc (g *guildService) FindUsersByIds(ids []string, guildId string) (*[]model.User, error) {\n\treturn g.GuildRepository.FindUsersByIds(ids, guildId)\n}\n\nfunc (g *guildService) UpdateMemberLastSeen(userId, guildId string) error {\n\treturn g.GuildRepository.UpdateMemberLastSeen(userId, guildId)\n}\n\nfunc (g *guildService) RemoveVCMember(userId, guildId string) error {\n\treturn g.GuildRepository.RemoveVCMember(userId, guildId)\n}\n\nfunc (g *guildService) GetVCMembers(guildId string) (*[]model.VCMemberResponse, error) {\n\treturn g.GuildRepository.VCMembers(guildId)\n}\n\nfunc (g *guildService) UpdateVCMember(isMuted, isDeafened bool, userId, guildId string) error {\n\treturn g.GuildRepository.UpdateVCMember(isMuted, isDeafened, userId, guildId)\n}\n\nfunc (g *guildService) GetVCMember(userId, guildId string) (*model.VCMember, error) {\n\treturn g.GuildRepository.GetVCMember(userId, guildId)\n}\n"
  },
  {
    "path": "server/service/guild_service_test.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"testing\"\n)\n\nfunc TestGuildService_CreateGuild(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tparams := &model.Guild{\n\t\t\tName: mockGuild.Name,\n\t\t}\n\n\t\tmockGuildRepository := new(mocks.GuildRepository)\n\t\tgs := NewGuildService(&GSConfig{\n\t\t\tGuildRepository: mockGuildRepository,\n\t\t})\n\n\t\tmockGuildRepository.\n\t\t\tOn(\"Create\", params).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockGuild.ID = uid\n\t\t\t}).Return(mockGuild, nil)\n\n\t\tguild, err := gs.CreateGuild(params)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, uid, mockGuild.ID)\n\t\tassert.Equal(t, guild, mockGuild)\n\n\t\tmockGuildRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockGuild := fixture.GetMockGuild(\"\")\n\n\t\tparams := &model.Guild{\n\t\t\tName: mockGuild.Name,\n\t\t}\n\n\t\tmockGuildRepository := new(mocks.GuildRepository)\n\t\tgs := NewGuildService(&GSConfig{\n\t\t\tGuildRepository: mockGuildRepository,\n\t\t})\n\n\t\tmockErr := apperrors.NewInternal()\n\n\t\tmockGuildRepository.\n\t\t\tOn(\"Create\", params).\n\t\t\tReturn(nil, mockErr)\n\n\t\tguild, err := gs.CreateGuild(params)\n\n\t\t// assert error is error we response with in mock\n\t\tassert.EqualError(t, err, mockErr.Error())\n\t\tassert.Nil(t, guild)\n\n\t\tmockGuildRepository.AssertExpectations(t)\n\t})\n}\n\nfunc TestGuildService_GenerateInviteLink(t *testing.T) {\n\tguildId := fixture.RandID()\n\tctx := context.TODO()\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockRedisRepository := new(mocks.RedisRepository)\n\t\tgs := NewGuildService(&GSConfig{\n\t\t\tRedisRepository: mockRedisRepository,\n\t\t})\n\n\t\targs := mock.Arguments{\n\t\t\tctx,\n\t\t\tguildId,\n\t\t\tmock.AnythingOfType(\"string\"),\n\t\t\tfalse,\n\t\t}\n\n\t\tmockRedisRepository.\n\t\t\tOn(\"SaveInvite\", args...).\n\t\t\tRun(func(args mock.Arguments) {}).\n\t\t\tReturn(nil)\n\n\t\tlink, err := gs.GenerateInviteLink(ctx, guildId, false)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.NotEqual(t, link, \"\")\n\n\t\tmockRedisRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockRedisRepository := new(mocks.RedisRepository)\n\t\tgs := NewGuildService(&GSConfig{\n\t\t\tRedisRepository: mockRedisRepository,\n\t\t})\n\n\t\targs := mock.Arguments{\n\t\t\tctx,\n\t\t\tguildId,\n\t\t\tmock.AnythingOfType(\"string\"),\n\t\t\tfalse,\n\t\t}\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockRedisRepository.\n\t\t\tOn(\"SaveInvite\", args...).\n\t\t\tRun(func(args mock.Arguments) {}).\n\t\t\tReturn(mockError)\n\n\t\tlink, err := gs.GenerateInviteLink(ctx, guildId, false)\n\n\t\tassert.Error(t, err)\n\n\t\tassert.Equal(t, link, \"\")\n\t\tassert.Equal(t, err, mockError)\n\n\t\tmockRedisRepository.AssertExpectations(t)\n\t})\n}\n"
  },
  {
    "path": "server/service/id_generator.go",
    "content": "package service\n\nimport (\n\t\"github.com/bwmarrin/snowflake\"\n\t\"log\"\n)\n\nvar node *snowflake.Node\n\nfunc init() {\n\tconst nodeID int64 = 1\n\n\tvar err error\n\tnode, err = snowflake.NewNode(nodeID)\n\tif err != nil {\n\t\tlog.Fatalf(\"failed to init snowflake node: %v\", err.Error())\n\t}\n}\n\n// GenerateId generates a snowflake id\nfunc GenerateId() string {\n\t// Generate a snowflake ID.\n\tid := node.Generate()\n\n\treturn id.String()\n}\n"
  },
  {
    "path": "server/service/message_service.go",
    "content": "package service\n\nimport (\n\t\"fmt\"\n\tgonanoid \"github.com/matoous/go-nanoid\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// messageService acts as a struct for injecting an implementation of MessageRepository\n// for use in service methods\ntype messageService struct {\n\tMessageRepository model.MessageRepository\n\tFileRepository    model.FileRepository\n}\n\n// MSConfig will hold repositories that will eventually be injected into\n// this service layer\ntype MSConfig struct {\n\tMessageRepository model.MessageRepository\n\tFileRepository    model.FileRepository\n}\n\n// NewMessageService is a factory function for\n// initializing a UserService with its repository layer dependencies\nfunc NewMessageService(c *MSConfig) model.MessageService {\n\treturn &messageService{\n\t\tMessageRepository: c.MessageRepository,\n\t\tFileRepository:    c.FileRepository,\n\t}\n}\n\nfunc (m *messageService) GetMessages(userId string, channel *model.Channel, cursor string) (*[]model.MessageResponse, error) {\n\treturn m.MessageRepository.GetMessages(userId, channel, cursor)\n}\n\nfunc (m *messageService) CreateMessage(params *model.Message) (*model.Message, error) {\n\tparams.ID = GenerateId()\n\n\treturn m.MessageRepository.CreateMessage(params)\n}\n\nfunc (m *messageService) UpdateMessage(message *model.Message) error {\n\treturn m.MessageRepository.UpdateMessage(message)\n}\n\nfunc (m *messageService) DeleteMessage(message *model.Message) error {\n\tif message.Attachment != nil {\n\t\tif err := m.FileRepository.DeleteImage(message.Attachment.Filename); err != nil {\n\t\t\tlog.Printf(\"Error deleting file from S3: %s\", err)\n\t\t}\n\t}\n\n\treturn m.MessageRepository.DeleteMessage(message)\n}\n\nfunc (m *messageService) UploadFile(header *multipart.FileHeader, channelId string) (*model.Attachment, error) {\n\n\tfilename := formatName(header.Filename)\n\tmimetype := header.Header.Get(\"Content-Type\")\n\n\tattachment := model.Attachment{\n\t\tFileType: mimetype,\n\t\tFilename: filename,\n\t}\n\n\tattachment.ID = GenerateId()\n\n\tdirectory := fmt.Sprintf(\"channels/%s\", channelId)\n\turl, err := m.FileRepository.UploadFile(header, directory, filename, mimetype)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tattachment.Url = url\n\n\treturn &attachment, nil\n}\n\nfunc (m *messageService) Get(messageId string) (*model.Message, error) {\n\treturn m.MessageRepository.GetById(messageId)\n}\n\nvar re = regexp.MustCompile(`/[^a-z0-9]/g`)\n\nfunc formatName(filename string) string {\n\text := path.Ext(filename)\n\tid, _ := gonanoid.Nanoid(5)\n\tfilename = strings.TrimSuffix(filename, filepath.Ext(filename))\n\tfilename = strings.ToLower(filename)\n\tfilename = re.ReplaceAllString(filename, \"-\")\n\treturn fmt.Sprintf(\"%s-%s%s\", id, filename, ext)\n}\n"
  },
  {
    "path": "server/service/message_service_test.go",
    "content": "package service\n\nimport (\n\t\"fmt\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"testing\"\n)\n\nfunc TestGuildService_CreateMessage(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\t\tmockMessage := fixture.GetMockMessage(\"\", \"\")\n\n\t\tparams := &model.Message{\n\t\t\tUserId:    mockMessage.UserId,\n\t\t\tChannelId: mockMessage.ChannelId,\n\t\t\tText:      mockMessage.Text,\n\t\t}\n\n\t\tmockMessageRepository := new(mocks.MessageRepository)\n\t\tms := NewMessageService(&MSConfig{\n\t\t\tMessageRepository: mockMessageRepository,\n\t\t})\n\n\t\tmockMessageRepository.\n\t\t\tOn(\"CreateMessage\", params).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockMessage.ID = uid\n\t\t\t}).Return(mockMessage, nil)\n\n\t\tmessage, err := ms.CreateMessage(params)\n\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, uid, mockMessage.ID)\n\t\tassert.Equal(t, message, mockMessage)\n\n\t\tmockMessageRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockMessage := fixture.GetMockMessage(\"\", \"\")\n\n\t\tparams := &model.Message{\n\t\t\tUserId:    mockMessage.UserId,\n\t\t\tChannelId: mockMessage.ChannelId,\n\t\t\tText:      mockMessage.Text,\n\t\t}\n\n\t\tmockMessageRepository := new(mocks.MessageRepository)\n\t\tms := NewMessageService(&MSConfig{\n\t\t\tMessageRepository: mockMessageRepository,\n\t\t})\n\n\t\tmockErr := apperrors.NewInternal()\n\t\tmockMessageRepository.\n\t\t\tOn(\"CreateMessage\", params).\n\t\t\tReturn(nil, mockErr)\n\n\t\tmessage, err := ms.CreateMessage(params)\n\n\t\tassert.EqualError(t, err, mockErr.Error())\n\t\tassert.Nil(t, message)\n\t})\n}\n\nfunc TestGuildService_DeleteMessage(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockMessage := fixture.GetMockMessage(\"\", \"\")\n\n\t\tmockMessageRepository := new(mocks.MessageRepository)\n\t\tms := NewMessageService(&MSConfig{\n\t\t\tMessageRepository: mockMessageRepository,\n\t\t})\n\n\t\tmockMessageRepository.\n\t\t\tOn(\"DeleteMessage\", mockMessage).\n\t\t\tReturn(nil)\n\n\t\terr := ms.DeleteMessage(mockMessage)\n\n\t\tassert.NoError(t, err)\n\n\t\tmockMessageRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Success with attachment\", func(t *testing.T) {\n\t\tmockMessage := fixture.GetMockMessage(\"\", \"\")\n\t\tmockMessage.Attachment = &model.Attachment{\n\t\t\tFilename: fixture.RandStr(12),\n\t\t}\n\n\t\tmockMessageRepository := new(mocks.MessageRepository)\n\t\tmockFileRepository := new(mocks.FileRepository)\n\t\tms := NewMessageService(&MSConfig{\n\t\t\tMessageRepository: mockMessageRepository,\n\t\t\tFileRepository:    mockFileRepository,\n\t\t})\n\n\t\tmockFileRepository.On(\"DeleteImage\", mockMessage.Attachment.Filename).Return(nil)\n\n\t\tmockMessageRepository.\n\t\t\tOn(\"DeleteMessage\", mockMessage).\n\t\t\tReturn(nil)\n\n\t\terr := ms.DeleteMessage(mockMessage)\n\n\t\tassert.NoError(t, err)\n\n\t\tmockMessageRepository.AssertExpectations(t)\n\t\tmockFileRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockMessage := fixture.GetMockMessage(\"\", \"\")\n\n\t\tmockMessageRepository := new(mocks.MessageRepository)\n\t\tms := NewMessageService(&MSConfig{\n\t\t\tMessageRepository: mockMessageRepository,\n\t\t})\n\n\t\tmockError := apperrors.NewInternal()\n\n\t\tmockMessageRepository.\n\t\t\tOn(\"DeleteMessage\", mockMessage).\n\t\t\tReturn(mockError)\n\n\t\terr := ms.DeleteMessage(mockMessage)\n\n\t\tassert.EqualError(t, err, mockError.Error())\n\n\t\tmockMessageRepository.AssertExpectations(t)\n\t})\n}\n\nfunc TestMessageService_UploadFile(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\timageURL := \"https://imageurl.com/jdfkj34kljl\"\n\t\tchannelId := fixture.RandID()\n\t\tid := GenerateId()\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.png\", \"image/png\")\n\t\tdefer multipartImageFixture.Close()\n\t\timageFileHeader := multipartImageFixture.GetFormFile()\n\t\tdirectory := fmt.Sprintf(\"channels/%s\", channelId)\n\n\t\tattachment := model.Attachment{\n\t\t\tFileType: imageFileHeader.Header.Get(\"Content-Type\"),\n\t\t\tFilename: formatName(\"image.png\"),\n\t\t}\n\n\t\tuploadFileArgs := mock.Arguments{\n\t\t\timageFileHeader,\n\t\t\tdirectory,\n\t\t\tmock.AnythingOfType(\"string\"),\n\t\t\tattachment.FileType,\n\t\t}\n\n\t\tmockFileRepository := new(mocks.FileRepository)\n\n\t\tmockFileRepository.\n\t\t\tOn(\"UploadFile\", uploadFileArgs...).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tattachment.ID = id\n\t\t\t}).\n\t\t\tReturn(imageURL, nil)\n\n\t\tms := NewMessageService(&MSConfig{\n\t\t\tFileRepository: mockFileRepository,\n\t\t})\n\n\t\t_, err := ms.UploadFile(imageFileHeader, channelId)\n\t\tassert.NoError(t, err)\n\n\t\tmockFileRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tchannelId := fixture.RandID()\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.png\", \"image/png\")\n\t\tdefer multipartImageFixture.Close()\n\t\timageFileHeader := multipartImageFixture.GetFormFile()\n\t\tdirectory := fmt.Sprintf(\"channels/%s\", channelId)\n\n\t\tattachment := model.Attachment{\n\t\t\tFileType: imageFileHeader.Header.Get(\"Content-Type\"),\n\t\t\tFilename: formatName(\"image.png\"),\n\t\t}\n\n\t\tuploadFileArgs := mock.Arguments{\n\t\t\timageFileHeader,\n\t\t\tdirectory,\n\t\t\tmock.AnythingOfType(\"string\"),\n\t\t\tattachment.FileType,\n\t\t}\n\n\t\tmockFileRepository := new(mocks.FileRepository)\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockFileRepository.\n\t\t\tOn(\"UploadFile\", uploadFileArgs...).\n\t\t\tReturn(\"\", mockError)\n\n\t\tms := NewMessageService(&MSConfig{\n\t\t\tFileRepository: mockFileRepository,\n\t\t})\n\n\t\tatt, err := ms.UploadFile(imageFileHeader, channelId)\n\t\tassert.Error(t, err)\n\t\tassert.Equal(t, err, apperrors.NewInternal())\n\t\tassert.Nil(t, att)\n\n\t\tmockFileRepository.AssertExpectations(t)\n\t})\n}\n"
  },
  {
    "path": "server/service/password.go",
    "content": "package service\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"golang.org/x/crypto/scrypt\"\n)\n\n// hashPassword hashes the given password using bcrypt\nfunc hashPassword(password string) (string, error) {\n\tsalt := make([]byte, 32)\n\t_, err := rand.Read(salt)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// using recommended cost parameters from - https://godoc.org/golang.org/x/crypto/scrypt\n\tshash, err := scrypt.Key([]byte(password), salt, 32768, 8, 1, 32)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// return hex-encoded string with salt appended to password\n\thashedPW := fmt.Sprintf(\"%s.%s\", hex.EncodeToString(shash), hex.EncodeToString(salt))\n\n\treturn hashedPW, nil\n}\n\n// comparePasswords compares the stored password with the supplied one\nfunc comparePasswords(storedPassword string, suppliedPassword string) (bool, error) {\n\tpwsalt := strings.Split(storedPassword, \".\")\n\n\tif len(pwsalt) < 2 {\n\t\treturn false, fmt.Errorf(\"did not provide a valid hash\")\n\t}\n\n\t// check supplied password salted with hash\n\tsalt, err := hex.DecodeString(pwsalt[1])\n\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to verify user password\")\n\t}\n\n\tshash, err := scrypt.Key([]byte(suppliedPassword), salt, 32768, 8, 1, 32)\n\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"unable to verify user password\")\n\t}\n\n\treturn hex.EncodeToString(shash) == pwsalt[0], nil\n}\n"
  },
  {
    "path": "server/service/password_test.go",
    "content": "package service\n\nimport (\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPassword(t *testing.T) {\n\tpassword := fixture.RandStringRunes(10)\n\n\thashedPassword1, err := hashPassword(password)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, hashedPassword1)\n\n\tvalid, err := comparePasswords(hashedPassword1, password)\n\trequire.NoError(t, err)\n\trequire.True(t, valid)\n\n\twrongPassword := fixture.RandStringRunes(10)\n\tvalid, err = comparePasswords(hashedPassword1, wrongPassword)\n\trequire.NoError(t, err)\n\trequire.False(t, valid)\n\n\thashedPassword2, err := hashPassword(password)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, hashedPassword2)\n\trequire.NotEqual(t, hashedPassword1, hashedPassword2)\n\n\tvalid, err = comparePasswords(password, hashedPassword1)\n\trequire.Error(t, err)\n\trequire.EqualError(t, err, \"did not provide a valid hash\")\n\trequire.False(t, valid)\n}\n"
  },
  {
    "path": "server/service/socket_service.go",
    "content": "package service\n\nimport (\n\t\"encoding/json\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/ws\"\n\t\"log\"\n)\n\ntype socketService struct {\n\tHub               ws.Hub\n\tGuildRepository   model.GuildRepository\n\tChannelRepository model.ChannelRepository\n}\n\n// SSConfig will hold repositories that will eventually be injected into\n// this service layer\ntype SSConfig struct {\n\tHub               ws.Hub\n\tGuildRepository   model.GuildRepository\n\tChannelRepository model.ChannelRepository\n}\n\n// NewSocketService is a factory function for\n// initializing a SocketService with its repository layer dependencies\nfunc NewSocketService(c *SSConfig) model.SocketService {\n\treturn &socketService{\n\t\tHub:               c.Hub,\n\t\tGuildRepository:   c.GuildRepository,\n\t\tChannelRepository: c.ChannelRepository,\n\t}\n}\n\nfunc (s *socketService) EmitNewMessage(room string, message *model.MessageResponse) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.NewMessageAction,\n\t\tData:   message,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n}\n\nfunc (s *socketService) EmitEditMessage(room string, message *model.MessageResponse) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.EditMessageAction,\n\t\tData:   message,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n}\n\nfunc (s *socketService) EmitDeleteMessage(room, messageId string) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.DeleteMessageAction,\n\t\tData:   messageId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n}\n\nfunc (s *socketService) EmitNewChannel(room string, channel *model.ChannelResponse) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.AddChannelAction,\n\t\tData:   channel,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n}\n\nfunc (s *socketService) EmitNewPrivateChannel(members []string, channel *model.ChannelResponse) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.AddPrivateChannelAction,\n\t\tData:   channel,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\tfor _, id := range members {\n\t\ts.Hub.BroadcastToRoom(data, id)\n\t}\n}\n\nfunc (s *socketService) EmitEditChannel(room string, channel *model.ChannelResponse) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.EditChannelAction,\n\t\tData:   channel,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n}\n\nfunc (s *socketService) EmitDeleteChannel(channel *model.Channel) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.DeleteChannelAction,\n\t\tData:   channel.ID,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, *channel.GuildID)\n}\n\nfunc (s *socketService) EmitEditGuild(guild *model.Guild) {\n\n\tresponse := guild.SerializeGuild(\"\")\n\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.EditGuildAction,\n\t\tData:   response,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\tmembers, err := s.GuildRepository.GetMemberIds(guild.ID)\n\n\tif err != nil {\n\t\tlog.Printf(\"error getting member ids: %v\\n\", err)\n\t}\n\n\tfor _, id := range *members {\n\t\ts.Hub.BroadcastToRoom(data, id)\n\t}\n}\n\nfunc (s *socketService) EmitDeleteGuild(guildId string, members []string) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.DeleteGuildAction,\n\t\tData:   guildId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\tfor _, id := range members {\n\t\ts.Hub.BroadcastToRoom(data, id)\n\t}\n}\n\nfunc (s *socketService) EmitRemoveFromGuild(memberId, guildId string) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.RemoveFromGuildAction,\n\t\tData:   guildId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, memberId)\n}\n\nfunc (s *socketService) EmitAddMember(room string, member *model.User) {\n\n\tresponse := model.MemberResponse{\n\t\tId:        member.ID,\n\t\tUsername:  member.Username,\n\t\tImage:     member.Image,\n\t\tIsOnline:  member.IsOnline,\n\t\tCreatedAt: member.CreatedAt,\n\t\tUpdatedAt: member.UpdatedAt,\n\t\tIsFriend:  false,\n\t}\n\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.AddMemberAction,\n\t\tData:   response,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n}\n\nfunc (s *socketService) EmitRemoveMember(room, memberId string) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.RemoveMemberAction,\n\t\tData:   memberId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n}\n\nfunc (s *socketService) EmitNewDMNotification(channelId string, user *model.User) {\n\n\tresponse := model.DirectMessage{\n\t\tId: channelId,\n\t\tUser: model.DMUser{\n\t\t\tId:       user.ID,\n\t\t\tUsername: user.Username,\n\t\t\tImage:    user.Image,\n\t\t\tIsOnline: user.IsOnline,\n\t\t\tIsFriend: false,\n\t\t},\n\t}\n\n\tnotification, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.NewDMNotificationAction,\n\t\tData:   response,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling notification: %v\\n\", err)\n\t}\n\n\tpushToTop, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.PushToTopAction,\n\t\tData:   channelId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\tmembers, err := s.ChannelRepository.GetDMMemberIds(channelId)\n\n\tif err != nil {\n\t\tlog.Printf(\"error getting member ids: %v\\n\", err)\n\t}\n\n\tfor _, id := range *members {\n\t\tif id != user.ID {\n\t\t\ts.Hub.BroadcastToRoom(notification, id)\n\t\t}\n\t\ts.Hub.BroadcastToRoom(pushToTop, id)\n\t}\n}\n\nfunc (s *socketService) EmitNewNotification(guildId, channelId string) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.NewNotificationAction,\n\t\tData:   guildId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\tmembers, err := s.GuildRepository.GetMemberIds(guildId)\n\n\tif err != nil {\n\t\tlog.Printf(\"error getting member ids: %v\\n\", err)\n\t}\n\n\tfor _, id := range *members {\n\t\ts.Hub.BroadcastToRoom(data, id)\n\t}\n\n\tnotification, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.NewNotificationAction,\n\t\tData:   channelId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling notification: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(notification, guildId)\n}\n\nfunc (s *socketService) EmitSendRequest(room string) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.SendRequestAction,\n\t\tData:   \"\",\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n}\n\nfunc (s *socketService) EmitAddFriendRequest(room string, request *model.FriendRequest) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.AddRequestAction,\n\t\tData:   request,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, room)\n\ts.EmitSendRequest(room)\n}\n\nfunc (s *socketService) EmitAddFriend(user, member *model.User) {\n\n\tuserResponse := model.Friend{\n\t\tId:       user.ID,\n\t\tUsername: user.Username,\n\t\tImage:    user.Image,\n\t\tIsOnline: user.IsOnline,\n\t}\n\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.AddFriendAction,\n\t\tData:   userResponse,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, member.ID)\n\n\tmemberResponse := model.Friend{\n\t\tId:       member.ID,\n\t\tUsername: member.Username,\n\t\tImage:    member.Image,\n\t\tIsOnline: member.IsOnline,\n\t}\n\n\tdata, err = json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.AddFriendAction,\n\t\tData:   memberResponse,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, user.ID)\n}\n\nfunc (s *socketService) EmitRemoveFriend(userId, memberId string) {\n\tdata, err := json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.RemoveFriendAction,\n\t\tData:   memberId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, userId)\n\n\tdata, err = json.Marshal(model.WebsocketMessage{\n\t\tAction: ws.RemoveFriendAction,\n\t\tData:   userId,\n\t})\n\n\tif err != nil {\n\t\tlog.Printf(\"error marshalling response: %v\\n\", err)\n\t}\n\n\ts.Hub.BroadcastToRoom(data, memberId)\n}\n"
  },
  {
    "path": "server/service/user_service.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"log\"\n\t\"mime/multipart\"\n\t\"strings\"\n)\n\n// UserService acts as a struct for injecting an implementation of UserRepository\n// for use in service methods\ntype userService struct {\n\tUserRepository  model.UserRepository\n\tFileRepository  model.FileRepository\n\tRedisRepository model.RedisRepository\n\tMailRepository  model.MailRepository\n}\n\n// USConfig will hold repositories that will eventually be injected into\n// this service layer\ntype USConfig struct {\n\tUserRepository  model.UserRepository\n\tFileRepository  model.FileRepository\n\tRedisRepository model.RedisRepository\n\tMailRepository  model.MailRepository\n}\n\n// NewUserService is a factory function for\n// initializing a UserService with its repository layer dependencies\nfunc NewUserService(c *USConfig) model.UserService {\n\treturn &userService{\n\t\tUserRepository:  c.UserRepository,\n\t\tFileRepository:  c.FileRepository,\n\t\tRedisRepository: c.RedisRepository,\n\t\tMailRepository:  c.MailRepository,\n\t}\n}\n\n// Get retrieves a user based on their uid\nfunc (s *userService) Get(uid string) (*model.User, error) {\n\treturn s.UserRepository.FindByID(uid)\n}\n\n// GetByEmail retrieves a user based on their email\nfunc (s *userService) GetByEmail(email string) (*model.User, error) {\n\n\t// Sanitize email\n\temail = strings.ToLower(email)\n\temail = strings.TrimSpace(email)\n\n\treturn s.UserRepository.FindByEmail(email)\n}\n\n// Register creates a user\nfunc (s *userService) Register(user *model.User) (*model.User, error) {\n\thashedPassword, err := hashPassword(user.Password)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to signup user for email: %v\\n\", user.Email)\n\t\treturn nil, apperrors.NewInternal()\n\t}\n\n\tuser.ID = GenerateId()\n\tuser.Image = generateAvatar(user.Email)\n\tuser.Password = hashedPassword\n\n\treturn s.UserRepository.Create(user)\n}\n\n// Login reaches out to the UserRepository check if the user exists\n// and then compares the supplied password with the provided password\n// if a valid email/password combo is provided, u will hold all\n// available user fields\nfunc (s *userService) Login(email, password string) (*model.User, error) {\n\tuser, err := s.UserRepository.FindByEmail(email)\n\n\t// Will return NotAuthorized to client to omit details of why\n\tif err != nil {\n\t\treturn nil, apperrors.NewAuthorization(apperrors.InvalidCredentials)\n\t}\n\n\t// verify\n\tmatch, err := comparePasswords(user.Password, password)\n\n\tif err != nil {\n\t\treturn nil, apperrors.NewInternal()\n\t}\n\n\tif !match {\n\t\treturn nil, apperrors.NewAuthorization(apperrors.InvalidCredentials)\n\t}\n\n\treturn user, nil\n}\n\nfunc (s *userService) UpdateAccount(u *model.User) error {\n\treturn s.UserRepository.Update(u)\n}\n\nfunc (s *userService) IsEmailAlreadyInUse(email string) bool {\n\tuser, err := s.UserRepository.FindByEmail(email)\n\n\tif err != nil {\n\t\treturn true\n\t}\n\n\treturn user.ID != \"\"\n}\n\nfunc (s *userService) ChangeAvatar(header *multipart.FileHeader, directory string) (string, error) {\n\treturn s.FileRepository.UploadAvatar(header, directory)\n}\n\nfunc (s *userService) DeleteImage(key string) error {\n\treturn s.FileRepository.DeleteImage(key)\n}\n\nfunc (s *userService) ChangePassword(currentPassword, newPassword string, user *model.User) error {\n\t// verify\n\tmatch, err := comparePasswords(user.Password, currentPassword)\n\n\tif err != nil {\n\t\treturn apperrors.NewInternal()\n\t}\n\n\tif !match {\n\t\treturn apperrors.NewAuthorization(apperrors.InvalidOldPassword)\n\t}\n\n\thashedPassword, err := hashPassword(newPassword)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to change password for email: %v\\n\", user.Email)\n\t\treturn apperrors.NewInternal()\n\t}\n\n\tuser.Password = hashedPassword\n\n\treturn s.UserRepository.Update(user)\n}\n\nfunc (s *userService) ForgotPassword(ctx context.Context, user *model.User) error {\n\ttoken, err := s.RedisRepository.SetResetToken(ctx, user.ID)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn s.MailRepository.SendResetMail(user.Email, token)\n}\n\nfunc (s *userService) ResetPassword(ctx context.Context, password string, token string) (*model.User, error) {\n\tid, err := s.RedisRepository.GetIdFromToken(ctx, token)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuser, err := s.UserRepository.FindByID(id)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thashedPassword, err := hashPassword(password)\n\n\tif err != nil {\n\t\tlog.Printf(\"Unable to reset password\")\n\t\treturn nil, apperrors.NewInternal()\n\t}\n\n\tuser.Password = hashedPassword\n\n\tif err = s.UserRepository.Update(user); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn user, nil\n}\n\nfunc (s *userService) GetFriendAndGuildIds(userId string) (*[]string, error) {\n\treturn s.UserRepository.GetFriendAndGuildIds(userId)\n}\n\nfunc (s *userService) GetRequestCount(userId string) (*int64, error) {\n\treturn s.UserRepository.GetRequestCount(userId)\n}\n\n// generateAvatar returns a gravatar using the md5 hash of the email\nfunc generateAvatar(email string) string {\n\thash := md5.Sum([]byte(email))\n\treturn fmt.Sprintf(\"https://gravatar.com/avatar/%s?d=identicon\", hex.EncodeToString(hash[:]))\n}\n"
  },
  {
    "path": "server/service/user_service_test.go",
    "content": "package service\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"github.com/sentrionic/valkyrie/mocks\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"github.com/sentrionic/valkyrie/model/apperrors\"\n\t\"github.com/sentrionic/valkyrie/model/fixture\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/mock\"\n\t\"testing\"\n)\n\nfunc TestGet(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\t\tmockUser := fixture.GetMockUser()\n\t\tmockUser.ID = uid\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository: mockUserRepository,\n\t\t})\n\t\tmockUserRepository.On(\"FindByID\", uid).Return(mockUser, nil)\n\n\t\tu, err := us.Get(uid)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, u, mockUser)\n\t\tmockUserRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository: mockUserRepository,\n\t\t})\n\n\t\tmockUserRepository.On(\"FindByID\", uid).Return(nil, fmt.Errorf(\"some error down the call chain\"))\n\n\t\tu, err := us.Get(uid)\n\n\t\tassert.Nil(t, u)\n\t\tassert.Error(t, err)\n\t\tmockUserRepository.AssertExpectations(t)\n\t})\n}\n\nfunc TestUserService_GetByEmail(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository: mockUserRepository,\n\t\t})\n\t\tmockUserRepository.On(\"FindByEmail\", mockUser.Email).Return(mockUser, nil)\n\n\t\tu, err := us.GetByEmail(mockUser.Email)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, u, mockUser)\n\t\tmockUserRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\temail := fixture.Email()\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository: mockUserRepository,\n\t\t})\n\n\t\tmockUserRepository.On(\"FindByEmail\", email).Return(nil, fmt.Errorf(\"some error down the call chain\"))\n\n\t\tu, err := us.GetByEmail(email)\n\n\t\tassert.Nil(t, u)\n\t\tassert.Error(t, err)\n\t\tmockUserRepository.AssertExpectations(t)\n\t})\n}\n\nfunc TestRegister(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\t\tmockUser := fixture.GetMockUser()\n\n\t\tinitial := &model.User{\n\t\t\tUsername: mockUser.Username,\n\t\t\tEmail:    mockUser.Email,\n\t\t\tPassword: mockUser.Password,\n\t\t}\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository: mockUserRepository,\n\t\t})\n\n\t\t// We can use Run method to modify the user when the Create method is called.\n\t\t//  We can then chain on a Return method to return no error\n\t\tmockUserRepository.\n\t\t\tOn(\"Create\", initial).\n\t\t\tRun(func(args mock.Arguments) {\n\t\t\t\tmockUser.ID = uid\n\t\t\t}).Return(mockUser, nil)\n\n\t\tuser, err := us.Register(initial)\n\n\t\tassert.NoError(t, err)\n\n\t\t// assert user now has a userID\n\t\tassert.Equal(t, uid, mockUser.ID)\n\t\tassert.Equal(t, user, mockUser)\n\n\t\tmockUserRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockUser := &model.User{\n\t\t\tEmail:    \"bob@bob.com\",\n\t\t\tUsername: \"bobby\",\n\t\t\tPassword: \"howdyhoneighbor!\",\n\t\t}\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository: mockUserRepository,\n\t\t})\n\n\t\tmockErr := apperrors.NewConflict(\"email\", \"bob@bob.com\")\n\n\t\t// We can use Run method to modify the user when the Create method is called.\n\t\t//  We can then chain on a Return method to return no error\n\t\tmockUserRepository.\n\t\t\tOn(\"Create\", mockUser).\n\t\t\tReturn(nil, mockErr)\n\n\t\tuser, err := us.Register(mockUser)\n\n\t\t// assert error is error we response with in mock\n\t\tassert.EqualError(t, err, mockErr.Error())\n\t\tassert.Nil(t, user)\n\n\t\tmockUserRepository.AssertExpectations(t)\n\t})\n}\n\nfunc TestLogin(t *testing.T) {\n\t// setup valid email/pw combo with hashed password to test method\n\t// response when provided password is invalid\n\tvalidPW := \"howdyhoneighbor!\"\n\thashedValidPW, _ := hashPassword(validPW)\n\tinvalidPW := \"howdyhodufus!\"\n\n\tmockUserRepository := new(mocks.UserRepository)\n\tus := NewUserService(&USConfig{\n\t\tUserRepository: mockUserRepository,\n\t})\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tmockUser.Password = hashedValidPW\n\n\t\tmockUserRepository.\n\t\t\tOn(\"FindByEmail\", mockUser.Email).Return(mockUser, nil)\n\n\t\tuser, err := us.Login(mockUser.Email, validPW)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, user, mockUser)\n\t\tmockUserRepository.AssertCalled(t, \"FindByEmail\", mockUser.Email)\n\t})\n\n\tt.Run(\"Invalid email/password combination\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\n\t\tmockUserResp := fixture.GetMockUser()\n\t\tmockUserResp.ID = uid\n\t\tmockUserResp.Password = hashedValidPW\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmockUserResp.Email,\n\t\t}\n\n\t\t// We can use Run method to modify the user when the Create method is called.\n\t\t//  We can then chain on a Return method to return no error\n\t\tmockUserRepository.\n\t\t\tOn(\"FindByEmail\", mockArgs...).Return(mockUserResp, nil)\n\n\t\tuser, err := us.Login(mockUserResp.Email, invalidPW)\n\n\t\tassert.Error(t, err)\n\t\tassert.EqualError(t, err, apperrors.InvalidCredentials)\n\t\tassert.Nil(t, user)\n\t\tmockUserRepository.AssertCalled(t, \"FindByEmail\", mockArgs...)\n\t})\n}\n\nfunc TestUpdateDetails(t *testing.T) {\n\tmockUserRepository := new(mocks.UserRepository)\n\tus := NewUserService(&USConfig{\n\t\tUserRepository: mockUserRepository,\n\t})\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\n\t\tmockUser := fixture.GetMockUser()\n\t\tmockUser.ID = uid\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockUserRepository.\n\t\t\tOn(\"Update\", mockArgs...).Return(nil)\n\n\t\terr := us.UpdateAccount(mockUser)\n\n\t\tassert.NoError(t, err)\n\t\tmockUserRepository.AssertCalled(t, \"Update\", mockArgs...)\n\t})\n\n\tt.Run(\"Failure\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\n\t\tmockUser := fixture.GetMockUser()\n\t\tmockUser.ID = uid\n\n\t\tmockArgs := mock.Arguments{\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockError := apperrors.NewInternal()\n\n\t\tmockUserRepository.\n\t\t\tOn(\"Update\", mockArgs...).Return(mockError)\n\n\t\terr := us.UpdateAccount(mockUser)\n\t\tassert.Error(t, err)\n\n\t\tapperror, ok := err.(*apperrors.Error)\n\t\tassert.True(t, ok)\n\t\tassert.Equal(t, apperrors.Internal, apperror.Type)\n\n\t\tmockUserRepository.AssertCalled(t, \"Update\", mockArgs...)\n\t})\n}\n\nfunc TestUserService_ChangeAvatar(t *testing.T) {\n\tmockUserRepository := new(mocks.UserRepository)\n\tmockFileRepository := new(mocks.FileRepository)\n\n\tus := NewUserService(&USConfig{\n\t\tUserRepository: mockUserRepository,\n\t\tFileRepository: mockFileRepository,\n\t})\n\n\tt.Run(\"Successful new image\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\n\t\t// does not have have imageURL\n\t\tmockUser := fixture.GetMockUser()\n\t\tmockUser.ID = uid\n\t\tmockUser.Image = \"\"\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.png\", \"image/png\")\n\t\tdefer multipartImageFixture.Close()\n\t\timageFileHeader := multipartImageFixture.GetFormFile()\n\t\tdirectory := \"test_dir\"\n\n\t\tuploadFileArgs := mock.Arguments{\n\t\t\timageFileHeader,\n\t\t\tdirectory,\n\t\t}\n\n\t\timageURL := \"https://imageurl.com/jdfkj34kljl\"\n\n\t\tmockFileRepository.\n\t\t\tOn(\"UploadAvatar\", uploadFileArgs...).\n\t\t\tReturn(imageURL, nil)\n\n\t\tupdateArgs := mock.Arguments{\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockUpdatedUser := &model.User{\n\t\t\tBaseModel: model.BaseModel{\n\t\t\t\tID:        mockUser.ID,\n\t\t\t\tCreatedAt: mockUser.CreatedAt,\n\t\t\t\tUpdatedAt: mockUser.UpdatedAt,\n\t\t\t},\n\t\t\tEmail:    mockUser.Email,\n\t\t\tUsername: mockUser.Username,\n\t\t\tImage:    imageURL,\n\t\t\tPassword: mockUser.Password,\n\t\t}\n\n\t\tmockUserRepository.\n\t\t\tOn(\"Update\", updateArgs...).\n\t\t\tReturn(nil)\n\n\t\turl, err := us.ChangeAvatar(imageFileHeader, directory)\n\t\tassert.NoError(t, err)\n\t\tmockUser.Image = url\n\n\t\terr = us.UpdateAccount(mockUser)\n\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, mockUpdatedUser, mockUser)\n\t\tmockFileRepository.AssertCalled(t, \"UploadAvatar\", uploadFileArgs...)\n\t\tmockUserRepository.AssertCalled(t, \"Update\", updateArgs...)\n\t})\n\n\tt.Run(\"Successful update image\", func(t *testing.T) {\n\t\timageURL := \"https://imageurl.com/jdfkj34kljl\"\n\t\tuid := GenerateId()\n\n\t\tmockUser := &model.User{\n\t\t\tEmail:    \"new@bob.com\",\n\t\t\tUsername: \"NewRobert\",\n\t\t\tImage:    imageURL,\n\t\t}\n\t\tmockUser.ID = uid\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.png\", \"image/png\")\n\t\tdefer multipartImageFixture.Close()\n\t\timageFileHeader := multipartImageFixture.GetFormFile()\n\t\tdirectory := \"test_dir\"\n\n\t\tuploadFileArgs := mock.Arguments{\n\t\t\timageFileHeader,\n\t\t\tdirectory,\n\t\t}\n\n\t\tdeleteImageArgs := mock.Arguments{\n\t\t\timageURL,\n\t\t}\n\n\t\tmockFileRepository.\n\t\t\tOn(\"UploadAvatar\", uploadFileArgs...).\n\t\t\tReturn(imageURL, nil)\n\n\t\tmockFileRepository.\n\t\t\tOn(\"DeleteImage\", deleteImageArgs...).\n\t\t\tReturn(nil)\n\n\t\tmockUpdatedUser := &model.User{\n\t\t\tEmail:    \"new@bob.com\",\n\t\t\tUsername: \"NewRobert\",\n\t\t\tImage:    imageURL,\n\t\t}\n\t\tmockUpdatedUser.ID = uid\n\n\t\tupdateArgs := mock.Arguments{\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockUserRepository.\n\t\t\tOn(\"Update\", updateArgs...).\n\t\t\tReturn(nil)\n\n\t\turl, err := us.ChangeAvatar(imageFileHeader, directory)\n\t\tassert.NoError(t, err)\n\t\terr = us.DeleteImage(mockUser.Image)\n\t\tassert.NoError(t, err)\n\n\t\tmockUser.Image = url\n\t\terr = us.UpdateAccount(mockUser)\n\t\tassert.NoError(t, err)\n\n\t\tassert.Equal(t, mockUpdatedUser, mockUser)\n\t\tmockFileRepository.AssertCalled(t, \"UploadAvatar\", uploadFileArgs...)\n\t\tmockFileRepository.AssertCalled(t, \"DeleteImage\", imageURL)\n\t\tmockUserRepository.AssertCalled(t, \"Update\", updateArgs...)\n\t})\n\n\tt.Run(\"FileRepository Error\", func(t *testing.T) {\n\t\t// need to create a new UserService and repository\n\t\t// because testify has no way to overwrite a mock's\n\t\t// \"On\" call.\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tmockFileRepository := new(mocks.FileRepository)\n\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository: mockUserRepository,\n\t\t\tFileRepository: mockFileRepository,\n\t\t})\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.png\", \"image/png\")\n\t\tdefer multipartImageFixture.Close()\n\t\timageFileHeader := multipartImageFixture.GetFormFile()\n\t\tdirectory := \"file_directory\"\n\n\t\tuploadFileArgs := mock.Arguments{\n\t\t\timageFileHeader,\n\t\t\tdirectory,\n\t\t}\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockFileRepository.\n\t\t\tOn(\"UploadAvatar\", uploadFileArgs...).\n\t\t\tReturn(\"\", mockError)\n\n\t\turl, err := us.ChangeAvatar(imageFileHeader, directory)\n\t\tassert.Equal(t, \"\", url)\n\t\tassert.Error(t, err)\n\n\t\tmockFileRepository.AssertCalled(t, \"UploadAvatar\", uploadFileArgs...)\n\t\tmockUserRepository.AssertNotCalled(t, \"Update\")\n\t})\n\n\tt.Run(\"UserRepository UpdateImage Error\", func(t *testing.T) {\n\t\tuid := GenerateId()\n\t\timageURL := \"https://imageurl.com/jdfkj34kljl\"\n\n\t\t// has imageURL\n\t\tmockUser := &model.User{\n\t\t\tEmail:    \"new@bob.com\",\n\t\t\tUsername: \"A New Bob!\",\n\t\t\tImage:    imageURL,\n\t\t}\n\t\tmockUser.ID = uid\n\n\t\tmultipartImageFixture := fixture.NewMultipartImage(\"image.png\", \"image/png\")\n\t\tdefer multipartImageFixture.Close()\n\t\timageFileHeader := multipartImageFixture.GetFormFile()\n\t\tdirectory := \"file_dir\"\n\n\t\tuploadFileArgs := mock.Arguments{\n\t\t\timageFileHeader,\n\t\t\tdirectory,\n\t\t}\n\n\t\tmockFileRepository.\n\t\t\tOn(\"UploadAvatar\", uploadFileArgs...).\n\t\t\tReturn(imageURL, nil)\n\n\t\tupdateArgs := mock.Arguments{\n\t\t\tmockUser,\n\t\t}\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockUserRepository.\n\t\t\tOn(\"Update\", updateArgs...).\n\t\t\tReturn(mockError)\n\n\t\turl, err := us.ChangeAvatar(imageFileHeader, directory)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, imageURL, url)\n\n\t\terr = us.UpdateAccount(mockUser)\n\n\t\tassert.Error(t, err)\n\t\tmockFileRepository.AssertCalled(t, \"UploadAvatar\", uploadFileArgs...)\n\t\tmockUserRepository.AssertCalled(t, \"Update\", updateArgs...)\n\t})\n}\n\nfunc TestUserService_ChangePassword(t *testing.T) {\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrentPassword := mockUser.Password\n\n\t\thashedPassword, err := hashPassword(currentPassword)\n\t\tassert.NoError(t, err)\n\t\tmockUser.Password = hashedPassword\n\t\tnewPassword := fixture.RandStringRunes(10)\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{UserRepository: mockUserRepository})\n\n\t\tmockUserRepository.On(\"Update\", mockUser).Return(nil)\n\n\t\terr = us.ChangePassword(currentPassword, newPassword, mockUser)\n\t\tassert.NoError(t, err)\n\n\t\tassert.NotEqual(t, mockUser.Password, newPassword)\n\t\tassert.NotEqual(t, mockUser.Password, hashedPassword)\n\n\t\tmockUserRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error verifying password\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrentPassword := mockUser.Password\n\t\tnewPassword := fixture.RandStringRunes(10)\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{UserRepository: mockUserRepository})\n\n\t\terr := us.ChangePassword(currentPassword, newPassword, mockUser)\n\t\tassert.Error(t, err)\n\n\t\tassert.Equal(t, err, apperrors.NewInternal())\n\n\t\tmockUserRepository.AssertNotCalled(t, \"Update\")\n\t})\n\n\tt.Run(\"Current Password is incorrect\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrentPassword := fixture.RandStringRunes(10)\n\n\t\thashedPassword, err := hashPassword(mockUser.Password)\n\t\tassert.NoError(t, err)\n\t\tmockUser.Password = hashedPassword\n\t\tnewPassword := fixture.RandStringRunes(10)\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{UserRepository: mockUserRepository})\n\n\t\terr = us.ChangePassword(currentPassword, newPassword, mockUser)\n\t\tassert.Error(t, err)\n\n\t\tassert.Equal(t, err, apperrors.NewAuthorization(apperrors.InvalidOldPassword))\n\n\t\tmockUserRepository.AssertNotCalled(t, \"Update\")\n\t})\n\n\tt.Run(\"Error returned from the repository\", func(t *testing.T) {\n\t\tmockUser := fixture.GetMockUser()\n\t\tcurrentPassword := mockUser.Password\n\n\t\thashedPassword, err := hashPassword(currentPassword)\n\t\tassert.NoError(t, err)\n\t\tmockUser.Password = hashedPassword\n\t\tnewPassword := fixture.RandStringRunes(10)\n\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tus := NewUserService(&USConfig{UserRepository: mockUserRepository})\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockUserRepository.On(\"Update\", mockUser).Return(mockError)\n\n\t\terr = us.ChangePassword(currentPassword, newPassword, mockUser)\n\t\tassert.Error(t, err)\n\n\t\tmockUserRepository.AssertExpectations(t)\n\t})\n}\n\nfunc TestUserService_ForgotPassword(t *testing.T) {\n\tmockUser := fixture.GetMockUser()\n\ttoken := fixture.RandStringRunes(10)\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockRedisRepository := new(mocks.RedisRepository)\n\t\tmockMailRepository := new(mocks.MailRepository)\n\n\t\tus := NewUserService(&USConfig{\n\t\t\tRedisRepository: mockRedisRepository,\n\t\t\tMailRepository:  mockMailRepository,\n\t\t})\n\n\t\tmockRedisRepository.On(\"SetResetToken\", mock.Anything, mockUser.ID).Return(token, nil)\n\t\tmockMailRepository.On(\"SendResetMail\", mockUser.Email, token).Return(nil)\n\n\t\terr := us.ForgotPassword(context.TODO(), mockUser)\n\t\tassert.NoError(t, err)\n\n\t\tmockRedisRepository.AssertExpectations(t)\n\t\tmockMailRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"Error\", func(t *testing.T) {\n\t\tmockRedisRepository := new(mocks.RedisRepository)\n\t\tmockMailRepository := new(mocks.MailRepository)\n\n\t\tus := NewUserService(&USConfig{\n\t\t\tRedisRepository: mockRedisRepository,\n\t\t\tMailRepository:  mockMailRepository,\n\t\t})\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockRedisRepository.On(\"SetResetToken\", mock.Anything, mockUser.ID).Return(\"\", mockError)\n\n\t\terr := us.ForgotPassword(context.TODO(), mockUser)\n\t\tassert.Error(t, err)\n\n\t\tmockRedisRepository.AssertExpectations(t)\n\t\tmockMailRepository.AssertNotCalled(t, \"SendResetMail\")\n\t})\n}\n\nfunc TestUserService_ResetPassword(t *testing.T) {\n\tmockUser := fixture.GetMockUser()\n\tpassword := fixture.RandStr(10)\n\ttoken := fixture.RandStr(10)\n\n\tt.Run(\"Success\", func(t *testing.T) {\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tmockRedisRepository := new(mocks.RedisRepository)\n\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository:  mockUserRepository,\n\t\t\tRedisRepository: mockRedisRepository,\n\t\t})\n\n\t\tmockRedisRepository.On(\"GetIdFromToken\", mock.Anything, token).Return(mockUser.ID, nil)\n\t\tmockUserRepository.On(\"FindByID\", mockUser.ID).Return(mockUser, nil)\n\t\tmockUserRepository.On(\"Update\", mockUser).Return(nil)\n\n\t\tuser, err := us.ResetPassword(context.TODO(), password, token)\n\t\tassert.NoError(t, err)\n\t\tassert.NotNil(t, user)\n\n\t\tmockUserRepository.AssertExpectations(t)\n\t\tmockRedisRepository.AssertExpectations(t)\n\t})\n\n\tt.Run(\"No id found\", func(t *testing.T) {\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tmockRedisRepository := new(mocks.RedisRepository)\n\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository:  mockUserRepository,\n\t\t\tRedisRepository: mockRedisRepository,\n\t\t})\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockRedisRepository.On(\"GetIdFromToken\", mock.Anything, token).Return(\"\", mockError)\n\n\t\tuser, err := us.ResetPassword(context.TODO(), password, token)\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, user)\n\n\t\tmockRedisRepository.AssertCalled(t, \"GetIdFromToken\", mock.Anything, token)\n\t\tmockUserRepository.AssertNotCalled(t, \"FindByID\")\n\t\tmockUserRepository.AssertNotCalled(t, \"Update\")\n\t})\n\n\tt.Run(\"No user found\", func(t *testing.T) {\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tmockRedisRepository := new(mocks.RedisRepository)\n\t\tid := fixture.RandID()\n\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository:  mockUserRepository,\n\t\t\tRedisRepository: mockRedisRepository,\n\t\t})\n\n\t\tmockError := apperrors.NewInternal()\n\t\tmockRedisRepository.On(\"GetIdFromToken\", mock.Anything, token).Return(id, nil)\n\t\tmockUserRepository.On(\"FindByID\", id).Return(nil, mockError)\n\n\t\tuser, err := us.ResetPassword(context.TODO(), password, token)\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, user)\n\n\t\tmockRedisRepository.AssertCalled(t, \"GetIdFromToken\", mock.Anything, token)\n\t\tmockUserRepository.AssertCalled(t, \"FindByID\", id)\n\t\tmockUserRepository.AssertNotCalled(t, \"Update\")\n\t})\n\n\tt.Run(\"Error returned from the repository\", func(t *testing.T) {\n\t\tmockUserRepository := new(mocks.UserRepository)\n\t\tmockRedisRepository := new(mocks.RedisRepository)\n\t\tmockError := apperrors.NewInternal()\n\n\t\tus := NewUserService(&USConfig{\n\t\t\tUserRepository:  mockUserRepository,\n\t\t\tRedisRepository: mockRedisRepository,\n\t\t})\n\n\t\tmockRedisRepository.On(\"GetIdFromToken\", mock.Anything, token).Return(mockUser.ID, nil)\n\t\tmockUserRepository.On(\"FindByID\", mockUser.ID).Return(mockUser, nil)\n\t\tmockUserRepository.On(\"Update\", mockUser).Return(mockError)\n\n\t\tuser, err := us.ResetPassword(context.TODO(), password, token)\n\t\tassert.Error(t, err)\n\t\tassert.Nil(t, user)\n\n\t\tmockUserRepository.AssertExpectations(t)\n\t\tmockRedisRepository.AssertExpectations(t)\n\t})\n}\n"
  },
  {
    "path": "server/static/asyncapi.yml",
    "content": "asyncapi: '2.4.0'\ninfo:\n  title: Valkyrie Websockets\n  version: '1.0.0'\n  description: >\n    This service is in charge of processing websocket events. Websockets are authenticated       using sessions. All received messages must be specified like this: \n    | { \"action\": \"joinRoom\", \"room\": \"123456789\", \"message\": \"username\"} |.\n    \n    Room is required to join a channel room, message can be used for additional arguments or information. Both are optional.\n    Emited messages are of form | { \"action\": \"new_message\", \"data\": object } |.\n\nservers:\n  production:\n    url: wss://api.valkyrieapp.xyz/ws\n    protocol: wss\n  development:\n    url: ws://localhost:4000/ws\n    protocol: ws\n    \nchannels:\n  /:\n    publish:\n      message:\n        oneOf:\n          - $ref: '#/components/messages/toggleOnline'\n          - $ref: '#/components/messages/toggleOffline'\n          - $ref: '#/components/messages/joinUser'\n          - $ref: '#/components/messages/joinChannel'\n          - $ref: '#/components/messages/joinGuild'\n          - $ref: '#/components/messages/startTyping'\n          - $ref: '#/components/messages/stopTyping'\n          - $ref: '#/components/messages/getRequestCount'\n          - $ref: '#/components/messages/leaveGuild'\n          - $ref: '#/components/messages/leaveRoom'\n    subscribe:\n      message:\n        oneOf:\n          - $ref: '#/components/messages/addChannel'\n          - $ref: '#/components/messages/deleteChannel'\n          - $ref: '#/components/messages/editChannel'\n          - $ref: '#/components/messages/editGuild'\n          - $ref: '#/components/messages/deleteGuild'\n          - $ref: '#/components/messages/addMember'\n          - $ref: '#/components/messages/removeMember'\n          - $ref: '#/components/messages/new_message'\n          - $ref: '#/components/messages/edit_message'\n          - $ref: '#/components/messages/delete_message'\n          - $ref: '#/components/messages/push_to_top'\n          - $ref: '#/components/messages/new_notification'\n          - $ref: '#/components/messages/toggle_online'\n          - $ref: '#/components/messages/toggle_offline'\n          - $ref: '#/components/messages/addToTyping'\n          - $ref: '#/components/messages/removeFromTyping'\n          - $ref: '#/components/messages/send_request'\n          - $ref: '#/components/messages/add_friend'\n          - $ref: '#/components/messages/remove_friend'\n          - $ref: '#/components/messages/requestCount'\n\ncomponents:\n  securitySchemes:\n    session:\n      type: httpApiKey\n      name: token\n      in: query\n\n  schemas:\n    attachment:\n      type: object\n      properties:\n        fallback:\n          type: string\n        color:\n          type: string\n        pretext:\n          type: string\n        author_name:\n          type: string\n        author_link:\n          type: string\n          format: uri\n        author_icon:\n          type: string\n          format: uri\n        title:\n          type: string\n        title_link:\n          type: string\n          format: uri\n        text:\n          type: string\n        fields:\n          type: array\n          items:\n            type: object\n            properties:\n              title:\n                type: string\n              value:\n                type: string\n              short:\n                type: boolean\n        image_url:\n          type: string\n          format: uri\n        thumb_url:\n          type: string\n          format: uri\n        footer:\n          type: string\n        footer_icon:\n          type: string\n          format: uri\n        ts:\n          type: number\n\n  messages:\n    addChannel:\n      summary: 'A channel was created.'\n      payload:\n        type: object\n        description: 'see ChannelResponse'\n        properties:\n          id:\n            type: string\n          name:\n            type: string\n          isPublic:\n            type: boolean\n          createdAt:\n            type: string\n          updatedAt:\n            type: string\n          hasNotification:\n            type: boolean\n\n    deleteChannel:\n      summary: 'A channel was deleted.'\n      payload:\n        type: string\n        properties:\n          id:\n            type: string\n\n    editChannel:\n      summary: 'A channel was edited'\n      payload:\n        type: object\n        description: 'see ChannelResponse'\n        properties:\n          id:\n            type: string\n          name:\n            type: string\n          isPublic:\n            type: boolean\n          createdAt:\n            type: string\n          updatedAt:\n            type: string\n          hasNotification:\n            type: boolean\n\n    editGuild:\n      summary: 'A guild was edited'\n      payload:\n        type: object\n        description: 'see Guild'\n        properties:\n          name:\n            type: string\n          icon:\n            type: string\n\n    deleteGuild:\n      summary: 'A guild was deleted.'\n      payload:\n        type: string\n        properties:\n          id:\n            type: string\n\n    addMember:\n      summary: 'A member got added to the guild.'\n      payload:\n        type: object\n        description: 'see MemberResponse'\n        properties:\n          id:\n            type: string\n          username:\n            type: string\n          image:\n            type: string\n          isOnline:\n            type: string\n          createdAt:\n            type: string\n          updatedAt:\n            type: string\n          isFriend:\n            type: string\n\n    removeMember:\n      summary: 'A member was removed from the guild.'\n      payload:\n        type: string\n        properties:\n          id:\n            type: string\n\n    new_message:\n      summary: 'A new message was sent to a channel.'\n      payload:\n        type: object\n        properties:\n          user:\n            type: object\n            description: see MemberResponse\n          id:\n            type: string\n          text:\n            type: string\n          url:\n            type: string\n          filetype:\n            type: string\n          createdAt:\n            type: string\n          updatedAt:\n            type: string\n\n    edit_message:\n      summary: 'A message in this channel was edited.'\n      payload:\n        type: object\n        properties:\n          user:\n            type: object\n            description: see MemberResponse\n          id:\n            type: string\n          text:\n            type: string\n          url:\n            type: string\n          filetype:\n            type: string\n          createdAt:\n            type: string\n          updatedAt:\n            type: string\n\n    delete_message:\n      summary: 'A message in this channel was deleted.'\n      payload:\n        type: string\n        properties:\n          id:\n            type: string\n\n    push_to_top:\n      summary: 'A notification that pushes the DM to the top of the list.'\n      payload:\n        type: string\n        properties:\n          dmChannelId:\n            type: string\n\n    toggle_online:\n      summary: 'A notification that the user went online. Gets emited to guild members that currently view the guild and friends of the user.'\n      payload:\n        type: string\n        properties:\n          userId:\n            type: string\n\n    toggle_offline:\n      summary: 'A notification that the user went offline. Gets emited to guild members that currently view the guild and friends of the user.'\n      payload:\n        type: string\n        properties:\n          userId:\n            type: string\n\n    new_notification:\n      summary: 'A new message notification, published to all guild members. Additionally sends the channelId to members that currently view the guild.'\n      payload:\n        type: string\n        properties:\n          channelId:\n            type: string\n          guildId:\n            type: string\n\n    addToTyping:\n      summary: 'Emits the username to the channel the user is currently typing in.'\n      payload:\n        type: string\n        properties:\n          username:\n            type: string\n\n    removeFromTyping:\n      summary: 'Emits the username to the channel the user was typing in.'\n      payload:\n        type: string\n        properties:\n          username:\n            type: string\n\n    send_request:\n      summary: 'Emits a notification that a friends request was received'\n\n    add_friend:\n      summary: 'Adds the added person to the friends list.'\n      payload:\n        type: object\n        description: 'see MemberResponse'\n        properties:\n          member:\n            type: object\n\n    remove_friend:\n      summary: 'Removes the former friend from the friends list.'\n      payload:\n        type: string\n        properties:\n          id:\n            type: string\n\n    requestCount:\n      summary: 'The amount of friends requests the user has'\n      payload:\n        type: number\n        properties:\n          count:\n            type: number\n\n    toggleOnline:\n      summary: 'Changes the users status to online and broadcasts it to all friends and guilds they are part of.'\n\n    toggleOffline:\n      summary: 'Changes the users status to offline and broadcasts it to all friends and guilds they are part of. Leaves all connected rooms.'\n\n    joinUser:\n      summary: 'Joins the users room. This room receives guild, DM & friend notifications'\n      payload:\n        type: string\n        properties:\n          userId:\n            type: string\n\n    joinChannel:\n      summary: 'Joins the channels room. Checks if the user is a member of said channel. Receives message & typing events.'\n      payload:\n        type: string\n        properties:\n          channelId:\n            type: string\n\n    joinGuild:\n      summary: 'Joins the guilds room. Requires member access. Receives guild member & channel events.'\n      payload:\n        type: string\n        properties:\n          guildId:\n            type: string\n\n    startTyping:\n      summary: 'Emits the username to the channel they are typing in.'\n      payload:\n        type: string\n        properties:\n          channelId:\n            type: string\n          username:\n            type: string\n\n    stopTyping:\n      summary: 'Removes the username from the channel they were typing in.'\n      payload:\n        type: string\n        properties:\n          channelId:\n            type: string\n          username:\n            type: string\n\n    getRequestCount:\n      summary: 'Gets the amount of friend requests the user has.'\n\n    leaveGuild:\n      summary: 'Leaves the guild room.'\n      payload:\n        type: string\n        properties:\n          guildId:\n            type: string\n\n    leaveRoom:\n      summary: 'Leaves the room.'\n      payload:\n        type: string\n        properties:\n          roomId:\n            type: string\n"
  },
  {
    "path": "server/static/index.html",
    "content": "<!DOCTYPE html>\n\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  \n\n  <title>Valkyrie Websockets \n    \n    \n    \n    <span>1.0.0</span>\n documentation</title>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  \n  <link href=\"css/main.min.css\" rel=\"stylesheet\">\n  \n</head>\n<body>\n  <div class=\"relative md:flex lg:flex xl:flex\">\n    \n\n\n<input id=\"burger-menu\" type=\"checkbox\" />\n<label for=\"burger-menu\" class=\"shadow\">\n  <div class=\"wrapper\">\n    <span></span>\n    <span></span>\n    <span></span>\n  </div>\n</label>\n\n<div class=\"sidebar-panel fixed top-0 left-0 bottom-0 w-64 bg-gray-200 font-sans pt-8 pr-4 pb-4 pl-4\">\n  <div class=\"sidebar-panel__content\">\n    \n      <h1 class=\"text-2xl font-thin\">\n        Valkyrie Websockets \n    \n    \n    \n    <span>1.0.0</span>\n\n      </h1>\n    \n    <ul class=\"text-sm mt-10 mt-2\">\n      <li class=\"mb-3\">\n        <a class=\"js-menu-item text-gray-700 no-underline\" href=\"#introduction\">Introduction</a>\n      </li>\n      \n        <li class=\"mb-3\">\n          <a class=\"js-menu-item text-gray-700 no-underline\" href=\"#servers\">Servers</a>\n        </li>\n      \n    </ul>\n\n    \n      <h2 class=\"text-xs uppercase text-gray-500 mt-10 mb-4 font-thin\">Operations</h2>\n      <ul class=\"text-sm mt-2\">\n        <!-- With tags in sidebar -->\n        \n          <!--Without tags in sidebar-->\n          \n            <li>\n              \n                <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#operation-publish-/\">\n                  <span class=\"bg-blue-600 font-bold h-6 no-underline text-white uppercase p-1 mr-2 rounded\" style=\"height: 21px;font-size: 11px;\" title=\"Publish\">\n                    Pub\n                  </span>\n                  \n                    <div style=\"display:inline-block;\">\n                      \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">/</span>\n\n                    </div>\n                  \n                </a>\n              \n\n              \n                <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#operation-subscribe-/\">\n                  <span class=\"bg-green-600 font-bold no-underline text-white uppercase p-1 mr-2 rounded\" style=\"height: 21px;font-size: 11px;\" title=\"Subscribe\">\n                    Sub\n                  </span>\n                  \n                    <div style=\"display:inline-block;\">\n                      \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">/</span>\n\n                    </div>\n                  \n                </a>\n              \n              </a>\n            </li>\n          \n        \n      </ul>\n\n      <h2 class=\"text-xs uppercase text-gray-500 mt-10 mb-4 font-thin\">Messages</h2>\n      <ul class=\"text-sm mt-2\">\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-toggleOnline\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">toggleOnline</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-toggleOffline\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">toggleOffline</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-joinUser\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">joinUser</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-joinChannel\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">joinChannel</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-joinGuild\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">joinGuild</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-startTyping\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">startTyping</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-stopTyping\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">stopTyping</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-getRequestCount\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">getRequestCount</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-leaveGuild\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">leaveGuild</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-leaveRoom\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">leaveRoom</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-addChannel\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">addChannel</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-deleteChannel\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">deleteChannel</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-editChannel\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">editChannel</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-editGuild\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">editGuild</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-deleteGuild\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">deleteGuild</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-addMember\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">addMember</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-removeMember\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">removeMember</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-new_message\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">new_message</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-edit_message\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">edit_message</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-delete_message\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">delete_message</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-push_to_top\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">push_to_top</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-new_notification\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">new_notification</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-toggle_online\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">toggle_online</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-toggle_offline\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">toggle_offline</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-addToTyping\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">addToTyping</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-removeFromTyping\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">removeFromTyping</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-send_request\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">send_request</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-add_friend\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">add_friend</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-remove_friend\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">remove_friend</span>\n\n            </div>\n          </a>\n        \n          <a class=\"js-menu-item flex break-words no-underline text-gray-700 mt-8 sm:mt-8 md:mt-3\" href=\"#message-requestCount\">\n            <div style=\"display:inline-block;\">\n              \n<span class=\"string-chunk\" style=\"padding-top: 2px;\">requestCount</span>\n\n            </div>\n          </a>\n        \n      </ul>\n    \n  </div>\n</div>\n\n    <div class=\"content-panel flex-1 leading-loose z-10\">\n      \n\n<div class=\"center-block p-8\">\n  <a name=\"introduction\"></a>\n\n  <h1>Valkyrie Websockets \n    \n    \n    \n    <span>1.0.0</span>\n</h1>\n  <div class=\"leading-normal mb-4\">\n    \n    \n  </div>\n\n  <div class=\"markdown\">\n    <p>This service is in charge of processing websocket events. Websockets are authenticated       using sessions. All messages send to the server must be specified like this:\n    <pre><code>{\n  \"action\": \"joinRoom\",\n  \"room\": \"123456789\",\n  \"message\": \"username\"\n}</code></pre>\nRoom is required to join a channel room, message can be used for additional arguments or information. Both are optional. Messages emitted from the server are of form: \n<pre><code>{\n  \"action\": \"new_message\",\n  \"message\": {\n    ... properties\n  }\n}</code></pre>\n</p>\n  </div>\n\n\n  <div class=\"leading-normal mb-4\">\n    \n  </div>\n</div>\n\n\n\n\n\n\n<a name=\"servers\"></a>\n<div class=\"center-block p-8\">\n\n  <h2 class=\"mb-4\">Servers</h2>\n\n  \n    <div class=\"shadow bg-gray-200 mb-3 px-4 py-3\">\n      <div class=\"flex\">\n        <div>\n          <div class=\"pr-4 font-mono\">\n            \n<span class=\"string-chunk\" >wss://api.valkyrieapp.xyz/ws</span>\n\n\n            \n    \n    <span class=\"bg-teal-500 font-sans font-bold no-underline text-white uppercase rounded ml-2\"\n          style=\"height: 20px;font-size: 11px;padding: 3px;\">wss</span>\n    \n\n          </div>\n          <div class=\"text-xs text-gray-600\">\n            \n          </div>\n\n          \n\n    \n\n\n\n        </div>\n      </div>\n\n      \n        <div>\n          \n        </div>\n      \n\n      \n    </div>\n  \n    <div class=\"shadow bg-gray-200 mb-3 px-4 py-3\">\n      <div class=\"flex\">\n        <div>\n          <div class=\"pr-4 font-mono\">\n            \n<span class=\"string-chunk\" >ws://localhost:8080/ws</span>\n\n\n            \n    \n    <span class=\"bg-teal-500 font-sans font-bold no-underline text-white uppercase rounded ml-2\"\n          style=\"height: 20px;font-size: 11px;padding: 3px;\">ws</span>\n    \n\n          </div>\n          <div class=\"text-xs text-gray-600\">\n            \n          </div>\n\n          \n\n    \n\n\n\n        </div>\n      </div>\n\n      \n        <div>\n          \n        </div>\n      \n\n      \n    </div>\n  \n\n</div>\n\n\n\n\n\n\n\n<a name=\"operations\"></a>\n<h2 class=\"mb-4 ml-8\">Operations</h2>\n\n\n  <div class=\"responsive-container\">\n    \n      \n\n<a name=\"operation-publish-/\"></a>\n<div class=\"center-block p-8\">\n  <div class=\"operation pt-8 pb-8\">\n    <h3 class=\"font-mono text-base\">\n      \n      <span class=\"font-mono border border-blue-500 text-blue-500 uppercase p-1 rounded\" title=\"Publish\">Pub</span>\n      \n      \n      \n<span class=\"string-chunk\" >/</span>\n\n    </h3>\n\n    <div class=\"mt-4 mb-4 markdown\"></div>\n\n    <p class=\"text-gray-500 text-sm\"></p>\n    <div class=\"mt-4 mb-4 markdown\"></div>\n\n    \n\n    \n\n\n\n\n    \n\n    \n\n    \n\n\n\n    \n    <p>Accepts <strong>one of</strong> the following messages:</p>\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#1</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">toggleOnline</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Changes the users status to online and broadcasts it to all friends and guilds they are part of.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#2</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">toggleOffline</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Changes the users status to offline and broadcasts it to all friends and guilds they are part of. Leaves all connected rooms.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#3</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">joinUser</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Joins the users room. This room receives guild, DM &amp; friend notifications</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >userId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#4</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">joinChannel</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Joins the channels room. Checks if the user is a member of said channel. Receives message &amp; typing events.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >channelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#5</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">joinGuild</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Joins the guilds room. Requires member access. Receives guild member &amp; channel events.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >guildId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#6</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">startTyping</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Emits the username to the channel they are typing in.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >channelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#7</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">stopTyping</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Removes the username from the channel they were typing in.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >channelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#8</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">getRequestCount</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Gets the amount of friend requests the user has.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#9</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">leaveGuild</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Leaves the guild room.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >guildId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#10</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">leaveRoom</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Leaves the room.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >roomId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n    \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n  </div>\n</div>\n\n<div class=\"right-block p-8\">\n  <h4 class=\"text-lg text-white mb-4\">Examples</h4>\n\n  \n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">toggleOnline</h5>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">toggleOffline</h5>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">joinUser</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">joinChannel</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">joinGuild</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">startTyping</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">stopTyping</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">getRequestCount</h5>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">leaveGuild</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">leaveRoom</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n</div>\n\n\n\n    \n  </div>\n  <div class=\"responsive-container\">\n    \n      \n\n<a name=\"operation-subscribe-/\"></a>\n<div class=\"center-block p-8\">\n  <div class=\"operation pt-8 pb-8\">\n    <h3 class=\"font-mono text-base\">\n      \n      \n      <span class=\"font-mono border border-green-600 text-green-600 uppercase p-1 rounded\"\n        title=\"Subscribe\">Sub</span>\n      \n      \n<span class=\"string-chunk\" >/</span>\n\n    </h3>\n\n    <div class=\"mt-4 mb-4 markdown\"></div>\n\n    <p class=\"text-gray-500 text-sm\"></p>\n    <div class=\"mt-4 mb-4 markdown\"></div>\n\n    \n\n    \n\n\n\n\n    \n\n    \n\n    \n\n\n\n    \n    <p>Accepts <strong>one of</strong> the following messages:</p>\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#1</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">addChannel</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A channel was created.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see ChannelResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >name</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >isPublic</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nboolean   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >hasNotification</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nboolean   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#2</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">deleteChannel</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A channel was deleted.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#3</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">editChannel</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A channel was edited</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see ChannelResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >name</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >isPublic</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nboolean   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >hasNotification</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nboolean   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#4</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">editGuild</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A guild was edited</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see Guild</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >name</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >icon</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#5</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">deleteGuild</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A guild was deleted.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#6</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">addMember</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A member got added to the guild.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see MemberResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >image</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >isOnline</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >isFriend</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#7</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">removeMember</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A member was removed from the guild.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#8</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">new_message</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A new message was sent to a channel.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >user</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see MemberResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-200 py-4 rounded\"\n    >\n         \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >text</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >url</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >filetype</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#9</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">edit_message</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A message in this channel was edited.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >user</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see MemberResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-200 py-4 rounded\"\n    >\n         \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >text</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >url</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >filetype</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#10</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">delete_message</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A message in this channel was deleted.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#11</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">push_to_top</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A notification that pushes the DM to the top of the list.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >dmChannelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#12</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">new_notification</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A new message notification, published to all guild members. Additionally sends the channelId to members that currently view the guild.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >channelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >guildId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#13</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">toggle_online</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A notification that the user went online. Gets emited to guild members that currently view the guild and friends of the user.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >userId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#14</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">toggle_offline</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A notification that the user went offline. Gets emited to guild members that currently view the guild and friends of the user.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >userId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#15</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">addToTyping</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Emits the username to the channel the user is currently typing in.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#16</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">removeFromTyping</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Emits the username to the channel the user was typing in.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#17</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">send_request</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Emits a notification that a friends request was received</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#18</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">add_friend</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Adds the added person to the friends list.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see MemberResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >member</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-200 py-4 rounded\"\n    >\n         \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#19</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">remove_friend</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Removes the former friend from the friends list.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#20</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">requestCount</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">The amount of friends requests the user has</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nnumber   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >count</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nnumber   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n    \n    \n\n    \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n  </div>\n</div>\n\n<div class=\"right-block p-8\">\n  <h4 class=\"text-lg text-white mb-4\">Examples</h4>\n\n  \n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">addChannel</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>{\n  \"id\": \"string\",\n  \"name\": \"string\",\n  \"isPublic\": true,\n  \"createdAt\": \"string\",\n  \"updatedAt\": \"string\",\n  \"hasNotification\": true\n}</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">deleteChannel</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">editChannel</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>{\n  \"id\": \"string\",\n  \"name\": \"string\",\n  \"isPublic\": true,\n  \"createdAt\": \"string\",\n  \"updatedAt\": \"string\",\n  \"hasNotification\": true\n}</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">editGuild</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>{\n  \"name\": \"string\",\n  \"icon\": \"string\"\n}</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">deleteGuild</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">addMember</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>{\n  \"id\": \"string\",\n  \"username\": \"string\",\n  \"image\": \"string\",\n  \"isOnline\": \"string\",\n  \"createdAt\": \"string\",\n  \"updatedAt\": \"string\",\n  \"isFriend\": \"string\"\n}</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">removeMember</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">new_message</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>{\n  \"user\": {},\n  \"id\": \"string\",\n  \"text\": \"string\",\n  \"url\": \"string\",\n  \"filetype\": \"string\",\n  \"createdAt\": \"string\",\n  \"updatedAt\": \"string\"\n}</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">edit_message</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>{\n  \"user\": {},\n  \"id\": \"string\",\n  \"text\": \"string\",\n  \"url\": \"string\",\n  \"filetype\": \"string\",\n  \"createdAt\": \"string\",\n  \"updatedAt\": \"string\"\n}</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">delete_message</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">push_to_top</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">new_notification</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">toggle_online</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">toggle_offline</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">addToTyping</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">removeFromTyping</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">send_request</h5>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">add_friend</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>{\n  \"member\": {}\n}</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">remove_friend</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"string\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n\n<form>\n  <h5 class=\"examples-uid text-orange-600 mt-4\">requestCount</h5>\n  \n    <div class=\"payload-examples mb-4 \">\n      <div class=\"js-prop cursor-pointer  flex property\">\n        <span class=\"px-2 mr-2 text-gray-200 text-sm border rounded focus:outline-none cursor-pointer\">Payload</span>\n        \n          <svg class=\"expand inline align-baseline\" version=\"1.1\" viewBox=\"0 0 24 24\" x=\"0\"\n            xmlns=\"http://www.w3.org/2000/svg\" y=\"0\">\n            <polygon points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"></polygon>\n          </svg>\n        \n      </div>\n      <div class=\"children payload-examples mt-4\">\n        \n        <pre class=\"hljs mb-4 border border-gray-800 rounded\"><code>\"\"</code></pre>\n        <h6 class=\"text-xs font-bold text-gray-700 italic\">This example has been generated automatically.</h6>\n        \n      </div>\n    </div>\n  \n\n  \n</form>\n\n\n  \n  \n</div>\n\n\n\n    \n  </div>\n\n\n\n\n\n<a name=\"messages\"></a>\n<h2 class=\"ml-8 mt-4\">Messages</h2>\n\n<div class=\"responsive-container\">\n  <div class=\"center-block p-8\">\n    <div class=\"all-messages pb-8\">\n      \n      <a name=\"message-toggleOnline\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#1</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">toggleOnline</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Changes the users status to online and broadcasts it to all friends and guilds they are part of.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-toggleOffline\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#2</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">toggleOffline</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Changes the users status to offline and broadcasts it to all friends and guilds they are part of. Leaves all connected rooms.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-joinUser\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#3</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">joinUser</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Joins the users room. This room receives guild, DM &amp; friend notifications</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >userId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-joinChannel\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#4</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">joinChannel</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Joins the channels room. Checks if the user is a member of said channel. Receives message &amp; typing events.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >channelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-joinGuild\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#5</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">joinGuild</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Joins the guilds room. Requires member access. Receives guild member &amp; channel events.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >guildId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-startTyping\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#6</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">startTyping</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Emits the username to the channel they are typing in.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >channelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-stopTyping\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#7</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">stopTyping</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Removes the username from the channel they were typing in.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >channelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-getRequestCount\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#8</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">getRequestCount</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Gets the amount of friend requests the user has.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-leaveGuild\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#9</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">leaveGuild</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Leaves the guild room.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >guildId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-leaveRoom\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#10</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">leaveRoom</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Leaves the room.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >roomId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-addChannel\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#11</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">addChannel</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A channel was created.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see ChannelResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >name</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >isPublic</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nboolean   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >hasNotification</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nboolean   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-deleteChannel\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#12</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">deleteChannel</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A channel was deleted.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-editChannel\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#13</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">editChannel</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A channel was edited</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see ChannelResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >name</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >isPublic</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nboolean   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >hasNotification</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nboolean   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-editGuild\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#14</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">editGuild</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A guild was edited</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see Guild</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >name</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >icon</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-deleteGuild\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#15</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">deleteGuild</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A guild was deleted.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-addMember\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#16</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">addMember</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A member got added to the guild.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see MemberResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >image</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >isOnline</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >isFriend</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-removeMember\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#17</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">removeMember</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A member was removed from the guild.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-new_message\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#18</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">new_message</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A new message was sent to a channel.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >user</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see MemberResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-200 py-4 rounded\"\n    >\n         \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >text</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >url</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >filetype</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-edit_message\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#19</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">edit_message</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A message in this channel was edited.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >user</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see MemberResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-200 py-4 rounded\"\n    >\n         \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >text</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >url</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >filetype</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >createdAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >updatedAt</span\n        >\n            \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-delete_message\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#20</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">delete_message</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A message in this channel was deleted.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-push_to_top\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#21</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">push_to_top</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A notification that pushes the DM to the top of the list.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >dmChannelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-new_notification\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#22</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">new_notification</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A new message notification, published to all guild members. Additionally sends the channelId to members that currently view the guild.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >channelId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n      \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >guildId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-toggle_online\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#23</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">toggle_online</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A notification that the user went online. Gets emited to guild members that currently view the guild and friends of the user.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >userId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-toggle_offline\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#24</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">toggle_offline</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">A notification that the user went offline. Gets emited to guild members that currently view the guild and friends of the user.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >userId</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-addToTyping\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#25</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">addToTyping</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Emits the username to the channel the user is currently typing in.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-removeFromTyping\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#26</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">removeFromTyping</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Emits the username to the channel the user was typing in.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >username</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-send_request\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#27</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">send_request</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Emits a notification that a friends request was received</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-add_friend\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#28</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">add_friend</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Adds the added person to the friends list.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"><p>see MemberResponse</p>\n</div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >member</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nobject   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-200 py-4 rounded\"\n    >\n         \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n      <p class=\"pl-6 mb-2 mt-4 text-xs text-gray-700\">\n        Additional properties are allowed.\n      </p>\n        \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-remove_friend\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#29</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">remove_friend</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">Removes the former friend from the friends list.</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >id</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nstring   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n      <a name=\"message-requestCount\"></a>\n      \n\n<div class=\"bg-gray-200 rounded p-4 mt-2\">\n  <div class=\"text-sm text-gray-700 mb-2\">\n    \n    <span class=\"text-gray-700 font-bold mr-2\">#30</span>\n    \n    \n      <span class=\"border text-orange-600 rounded text-s py-0 px-2\">requestCount</span>\n    \n  </div>\n  <p class=\"text-gray-600 text-sm\">The amount of friends requests the user has</p>\n  \n  \n\n<div class=\"mt-4\">\n  \n</div>\n\n\n\n\n  \n  \n\n  <div class=\"mt-4 mb-4 markdown\"></div>\n  \n    \n<div>\n  \n\n<div\n  class=\"bg-gray-200  rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\"js-prop cursor-pointer py-2 flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >Payload</span\n        >\n          \n        <svg\n          class=\"expand inline align-baseline\"\n          version=\"1.1\"\n          viewBox=\"0 0 24 24\"\n          x=\"0\"\n          xmlns=\"http://www.w3.org/2000/svg\"\n          y=\"0\"\n        >\n          <polygon\n            points=\"17.3 8.3 12 13.6 6.7 8.3 5.3 9.7 12 16.4 18.7 9.7 \"\n          ></polygon>\n        </svg>\n          \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nnumber   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n    <div\n      class=\"children bg-gray-100 py-4 rounded\"\n    >\n           \n\n<div\n  class=\"bg-gray-100 pl-8 pr-8 rounded\"\n>\n  <div class=\"\">\n    <div\n      class=\" flex property\"\n    >\n      <div class=\"pr-4\" style=\"margin-top: -2px; min-width: 25%\">\n        <span\n          class=\"text-sm\"\n          style=\"word-break: break-word\"\n          >count</span\n        >\n         \n        <span class=\"border text-orange-600 rounded text-xs ml-3 py-0 px-2\"\n          ></span\n        >\n           \n      </div>\n\n      <div>\n        <div class=\"capitalize text-sm text-teal-500 font-bold\">\n          \nnumber   \n          <div class=\"inline-block\">\n                      \n          </div>\n        </div>\n\n        \n\n<div class=\"text-sm markdown\"></div>\n\n    \n      </div>\n    </div>\n\n    \n  </div>\n</div>\n\n    \n\n \n\n\n   \n       \n    </div>\n    \n  </div>\n</div>\n\n\n</div>\n\n  \n  \n\n  \n\n    \n\n\n  \n\n \n\n\n\n\n</div>\n\n\n      \n    </div>\n  </div>\n</div>\n\n\n    </div>\n    <div class=\"examples-panel absolute top-0 right-0 bottom-0\"></div>\n  </div>\n  \n  <script src=\"js/highlight.min.js\" type=\"application/javascript\"></script>\n  <script src=\"js/main.js\" type=\"application/javascript\"></script>\n  \n</body>\n</html>\n"
  },
  {
    "path": "server/static/js/main.js",
    "content": "/* eslint-disable */\n\nfunction bindExpanders() {\n  var props = document.querySelectorAll('.js-prop');\n  for (let index = 0; index < props.length; index++) {\n    const prop = props[index];\n    prop.addEventListener('click', function (ev) {\n      ev.stopPropagation();\n      ev.currentTarget.parentElement.classList.toggle('is-open');\n    });\n  }\n}\n\nfunction highlightCode() {\n  var blocks = document.querySelectorAll('.hljs code');\n\n  for (var i = 0; i < blocks.length; i++) {\n    hljs.highlightBlock(blocks[i]);\n  }\n}\n\nfunction bindMenuItems() {\n  var items = document.querySelectorAll('.js-menu-item');\n\n  for (var i = 0; i < items.length; i++) {\n    items[i].addEventListener('click', function () {\n      document.getElementById(\"burger-menu\").checked = false;\n    });\n  }\n}\n\nwindow.addEventListener('load', highlightCode);\nwindow.addEventListener('load', bindExpanders);\nwindow.addEventListener('load', bindMenuItems);\n"
  },
  {
    "path": "server/ws/actions.go",
    "content": "package ws\n\n// Subscribed Messages\nconst (\n\tJoinUserAction        = \"joinUser\"\n\tJoinGuildAction       = \"joinGuild\"\n\tJoinChannelAction     = \"joinChannel\"\n\tJoinVoiceAction       = \"joinVoice\"\n\tLeaveGuildAction      = \"leaveGuild\"\n\tLeaveRoomAction       = \"leaveRoom\"\n\tLeaveVoiceAction      = \"leaveVoice\"\n\tStartTypingAction     = \"startTyping\"\n\tStopTypingAction      = \"stopTyping\"\n\tToggleOnlineAction    = \"toggleOnline\"\n\tToggleOfflineAction   = \"toggleOffline\"\n\tGetRequestCountAction = \"getRequestCount\"\n)\n\n// Emitted Messages\nconst (\n\tNewMessageAction        = \"new_message\"\n\tEditMessageAction       = \"edit_message\"\n\tDeleteMessageAction     = \"delete_message\"\n\tAddChannelAction        = \"add_channel\"\n\tAddPrivateChannelAction = \"add_private_channel\"\n\tEditChannelAction       = \"edit_channel\"\n\tDeleteChannelAction     = \"delete_channel\"\n\tEditGuildAction         = \"edit_guild\"\n\tDeleteGuildAction       = \"delete_guild\"\n\tRemoveFromGuildAction   = \"remove_from_guild\"\n\tAddMemberAction         = \"add_member\"\n\tRemoveMemberAction      = \"remove_member\"\n\tNewDMNotificationAction = \"new_dm_notification\"\n\tNewNotificationAction   = \"new_notification\"\n\tToggleOnlineEmission    = \"toggle_online\"\n\tToggleOfflineEmission   = \"toggle_offline\"\n\tAddToTypingAction       = \"addToTyping\"\n\tRemoveFromTypingAction  = \"removeFromTyping\"\n\tSendRequestAction       = \"send_request\"\n\tAddRequestAction        = \"add_request\"\n\tAddFriendAction         = \"add_friend\"\n\tRemoveFriendAction      = \"remove_friend\"\n\tPushToTopAction         = \"push_to_top\"\n\tRequestCountEmission    = \"requestCount\"\n\tVoiceSignal             = \"voice-signal\"\n\tToggleMute              = \"toggle-mute\"\n\tToggleDeafen            = \"toggle-deafen\"\n)\n"
  },
  {
    "path": "server/ws/client.go",
    "content": "package ws\n\nimport (\n\t\"encoding/json\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"log\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n)\n\nconst (\n\t// Max wait time when writing message to peer\n\twriteWait = 10 * time.Second\n\n\t// Max time till next pong from peer\n\tpongWait = 60 * time.Second\n\n\t// Send ping interval, must be less then pong wait time\n\tpingPeriod = (pongWait * 9) / 10\n\n\t// Maximum message size allowed from peer.\n\tmaxMessageSize = 10000\n)\n\nvar newline = []byte{'\\n'}\n\nvar upgrader = websocket.Upgrader{\n\tReadBufferSize:  4096,\n\tWriteBufferSize: 4096,\n\tCheckOrigin: func(r *http.Request) bool {\n\t\treturn true\n\t},\n}\n\n// Client represents the websockets client at the server\ntype Client struct {\n\t// The actual websockets connection.\n\tID    string\n\tconn  *websocket.Conn\n\thub   *Hub\n\tsend  chan []byte\n\trooms map[*Room]bool\n}\n\nfunc newClient(conn *websocket.Conn, hub *Hub, id string) *Client {\n\treturn &Client{\n\t\tID:    id,\n\t\tconn:  conn,\n\t\thub:   hub,\n\t\tsend:  make(chan []byte, 256),\n\t\trooms: make(map[*Room]bool),\n\t}\n}\n\nfunc (client *Client) readPump() {\n\tdefer func() {\n\t\tclient.disconnect()\n\t}()\n\n\tclient.conn.SetReadLimit(maxMessageSize)\n\n\t_ = client.conn.SetReadDeadline(time.Now().Add(pongWait))\n\n\tclient.conn.SetPongHandler(func(string) error {\n\t\t_ = client.conn.SetReadDeadline(time.Now().Add(pongWait))\n\t\treturn nil\n\t})\n\n\t// Start endless read loop, waiting for messages from client\n\tfor {\n\t\t_, jsonMessage, err := client.conn.ReadMessage()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tclient.handleNewMessage(jsonMessage)\n\t}\n\n}\n\nfunc (client *Client) writePump() {\n\tticker := time.NewTicker(pingPeriod)\n\tdefer func() {\n\t\tticker.Stop()\n\t\t_ = client.conn.Close()\n\t}()\n\tfor {\n\t\tselect {\n\t\tcase message, ok := <-client.send:\n\t\t\t_ = client.conn.SetWriteDeadline(time.Now().Add(writeWait))\n\t\t\tif !ok {\n\t\t\t\t// The hub closed the channel.\n\t\t\t\t_ = client.conn.WriteMessage(websocket.CloseMessage, []byte{})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tw, err := client.conn.NextWriter(websocket.TextMessage)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_, _ = w.Write(message)\n\n\t\t\t// Attach queued chat messages to the current websockets message.\n\t\t\tn := len(client.send)\n\t\t\tfor i := 0; i < n; i++ {\n\t\t\t\t_, _ = w.Write(newline)\n\t\t\t\t_, _ = w.Write(<-client.send)\n\t\t\t}\n\n\t\t\tif err := w.Close(); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-ticker.C:\n\t\t\t_ = client.conn.SetWriteDeadline(time.Now().Add(writeWait))\n\t\t\tif err := client.conn.WriteMessage(websocket.PingMessage, nil); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (client *Client) disconnect() {\n\tclient.hub.unregister <- client\n\tfor room := range client.rooms {\n\t\troom.unregister <- client\n\t}\n\tclose(client.send)\n\t_ = client.conn.Close()\n}\n\n// ServeWs handles websockets requests from clients requests.\nfunc ServeWs(hub *Hub, ctx *gin.Context) {\n\n\tuserId := ctx.MustGet(\"userId\").(string)\n\tconn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil)\n\tif err != nil {\n\t\tlog.Println(err)\n\t\treturn\n\t}\n\n\tclient := newClient(conn, hub, userId)\n\n\tgo client.writePump()\n\tgo client.readPump()\n\n\thub.register <- client\n}\n\nfunc (client *Client) handleNewMessage(jsonMessage []byte) {\n\n\tvar message model.ReceivedMessage\n\tif err := json.Unmarshal(jsonMessage, &message); err != nil {\n\t\tlog.Printf(\"Error on unmarshal JSON message %s\", err)\n\t}\n\n\tswitch message.Action {\n\t// Join Room Actions\n\tcase JoinChannelAction:\n\t\tclient.handleJoinChannelMessage(message)\n\tcase JoinGuildAction:\n\t\tclient.handleJoinGuildMessage(message)\n\tcase JoinUserAction:\n\t\tclient.handleJoinRoomMessage(message)\n\tcase JoinVoiceAction:\n\t\tclient.handleJoinVoiceMessage(message)\n\n\t// Leave Room Actions\n\tcase LeaveRoomAction:\n\t\tclient.handleLeaveRoomMessage(message)\n\tcase LeaveGuildAction:\n\t\tclient.handleLeaveGuildMessage(message)\n\tcase LeaveVoiceAction:\n\t\tclient.handleLeaveVoiceMessage(message)\n\n\t// Chat Typing Actions\n\tcase StartTypingAction:\n\t\tclient.handleTypingEvent(message, AddToTypingAction)\n\tcase StopTypingAction:\n\t\tclient.handleTypingEvent(message, RemoveFromTypingAction)\n\n\t// Online Status Actions\n\tcase ToggleOnlineAction:\n\t\tclient.toggleOnlineStatus(true)\n\tcase ToggleOfflineAction:\n\t\tclient.toggleOnlineStatus(false)\n\n\t// Other\n\tcase GetRequestCountAction:\n\t\tclient.handleGetRequestCount()\n\n\t// Voice Chat\n\tcase VoiceSignal:\n\t\tclient.handleVoiceSignal(message)\n\n\tcase ToggleMute:\n\t\tfallthrough\n\tcase ToggleDeafen:\n\t\tclient.updateVCMember(message)\n\t}\n}\n\n// handleJoinChannelMessage joins the given room if the user is a member in it\nfunc (client *Client) handleJoinChannelMessage(message model.ReceivedMessage) {\n\troomName := message.Room\n\n\tcs := client.hub.channelService\n\tchannel, err := cs.Get(roomName)\n\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Check if the user has access to the given channel\n\tif err = cs.IsChannelMember(channel, client.ID); err != nil {\n\t\treturn\n\t}\n\n\tclient.handleJoinRoomMessage(message)\n}\n\n// handleJoinGuildMessage joins the given guild if the user is member in it\nfunc (client *Client) handleJoinGuildMessage(message model.ReceivedMessage) {\n\troomName := message.Room\n\n\tgs := client.hub.guildService\n\tguild, err := gs.GetGuild(roomName)\n\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// Check if the user is member of the given guild\n\tif !isMember(guild, client.ID) {\n\t\treturn\n\t}\n\n\tclient.handleJoinRoomMessage(message)\n}\n\n// handleJoinRoomMessage joins the given room\nfunc (client *Client) handleJoinRoomMessage(message model.ReceivedMessage) {\n\troomName := message.Room\n\n\troom := client.hub.findRoomById(roomName)\n\tif room == nil {\n\t\troom = client.hub.createRoom(roomName)\n\t}\n\n\tclient.rooms[room] = true\n\n\troom.register <- client\n}\n\n// handleLeaveGuildMessage leaves the room and updates the members last seen date\nfunc (client *Client) handleLeaveGuildMessage(message model.ReceivedMessage) {\n\t_ = client.hub.guildService.UpdateMemberLastSeen(client.ID, message.Room)\n\tclient.handleLeaveRoomMessage(message)\n}\n\n// handleLeaveRoomMessage leaves the room\nfunc (client *Client) handleLeaveRoomMessage(message model.ReceivedMessage) {\n\troom := client.hub.findRoomById(message.Room)\n\tdelete(client.rooms, room)\n\n\tif room != nil {\n\t\troom.unregister <- client\n\t}\n}\n\n// handleGetRequestCount returns the users incoming friend request count\nfunc (client *Client) handleGetRequestCount() {\n\tif room := client.hub.findRoomById(client.ID); room != nil {\n\t\tcount, err := client.hub.userService.GetRequestCount(client.ID)\n\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tmsg := model.WebsocketMessage{\n\t\t\tAction: RequestCountEmission,\n\t\t\tData:   count,\n\t\t}\n\t\troom.broadcast <- &msg\n\t}\n}\n\n// handleTypingEvent emits the username of the currently typing user to the room\nfunc (client *Client) handleTypingEvent(message model.ReceivedMessage, action string) {\n\troomID := message.Room\n\tif room := client.hub.findRoomById(roomID); room != nil {\n\t\tmsg := model.WebsocketMessage{\n\t\t\tAction: action,\n\t\t\tData:   message.Message,\n\t\t}\n\t\troom.broadcast <- &msg\n\t}\n}\n\n// toggleOnlineStatus updates the users online status and emits it to all\n// guilds the user is a member of and all of their friends\nfunc (client *Client) toggleOnlineStatus(isOnline bool) {\n\tuid := client.ID\n\tus := client.hub.userService\n\n\tuser, err := us.Get(uid)\n\n\tif err != nil {\n\t\tlog.Printf(\"could not find user: %v\", err)\n\t\treturn\n\t}\n\n\tuser.IsOnline = isOnline\n\n\tif err := us.UpdateAccount(user); err != nil {\n\t\tlog.Printf(\"could not update user: %v\", err)\n\t\treturn\n\t}\n\n\tids, err := us.GetFriendAndGuildIds(uid)\n\n\tif err != nil {\n\t\tlog.Printf(\"could not find ids: %v\", err)\n\t\treturn\n\t}\n\n\taction := ToggleOfflineEmission\n\tif isOnline {\n\t\taction = ToggleOnlineEmission\n\t}\n\n\tfor _, id := range *ids {\n\t\tif room := client.hub.findRoomById(id); room != nil {\n\t\t\tmsg := model.WebsocketMessage{\n\t\t\t\tAction: action,\n\t\t\t\tData:   uid,\n\t\t\t}\n\t\t\troom.broadcast <- &msg\n\t\t}\n\t}\n}\n\n// handleJoinGuildMessage joins the given guild's voice chat if the user is a member in it\nfunc (client *Client) handleJoinVoiceMessage(message model.ReceivedMessage) {\n\troomName := message.Room\n\n\troom := client.hub.findRoomById(roomName)\n\tif room == nil {\n\t\troom = client.hub.createRoom(roomName)\n\t}\n\n\tclient.rooms[room] = true\n\n\troom.register <- client\n\n\tuid := client.ID\n\tus := client.hub.userService\n\n\tuser, err := us.Get(uid)\n\n\tif err != nil {\n\t\tlog.Printf(\"could not find user: %v\", err)\n\t\treturn\n\t}\n\n\tguild, err := client.hub.guildService.GetGuild(room.GetId())\n\n\tif err != nil {\n\t\tlog.Printf(\"could not find guild: %v\", err)\n\t\treturn\n\t}\n\n\tif !isMember(guild, user.ID) {\n\t\treturn\n\t}\n\n\tguild.VCMembers = append(guild.VCMembers, *user)\n\n\t_ = client.hub.guildService.UpdateGuild(guild)\n\n\tclients, err := client.hub.guildService.GetVCMembers(guild.ID)\n\n\tif err != nil {\n\t\tlog.Printf(\"could not get vc members: %v\", err)\n\t\treturn\n\t}\n\n\tmsg := model.WebsocketMessage{\n\t\tAction: message.Action,\n\t\tData: gin.H{\n\t\t\t\"userId\":  user.ID,\n\t\t\t\"clients\": clients,\n\t\t},\n\t}\n\n\troom.broadcast <- &msg\n}\n\n// handleVoiceSignal exchanges the messages needed to setup WebRTC\nfunc (client *Client) handleVoiceSignal(message model.ReceivedMessage) {\n\tdata := (*message.Message).(map[string]any)\n\treceiver := data[\"userId\"]\n\n\tif receiver == \"\" {\n\t\treturn\n\t}\n\n\tdata[\"userId\"] = client.ID\n\n\tif room := client.hub.findRoomById(message.Room); room != nil {\n\t\tfor c := range room.clients {\n\t\t\tif c.ID == receiver {\n\t\t\t\tmsg := model.WebsocketMessage{\n\t\t\t\t\tAction: message.Action,\n\t\t\t\t\tData:   data,\n\t\t\t\t}\n\t\t\t\troom.broadcast <- &msg\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\n// handleLeaveVoiceMessage leaves the voice chat and the room\nfunc (client *Client) handleLeaveVoiceMessage(message model.ReceivedMessage) {\n\n\tif room := client.hub.findRoomById(message.Room); room != nil {\n\n\t\t_ = client.hub.guildService.RemoveVCMember(client.ID, message.Room)\n\t\tclient.handleLeaveRoomMessage(message)\n\n\t\tguild, err := client.hub.guildService.GetGuild(room.GetId())\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"could not find guild: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tclients, err := client.hub.guildService.GetVCMembers(guild.ID)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"could not get vc members: %v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tmsg := model.WebsocketMessage{\n\t\t\tAction: message.Action,\n\t\t\tData: gin.H{\n\t\t\t\t\"userId\":  client.ID,\n\t\t\t\t\"clients\": clients,\n\t\t\t},\n\t\t}\n\n\t\troom.broadcast <- &msg\n\n\t}\n}\n\n// updateVCMember updates the values of the user in the voice chat\nfunc (client *Client) updateVCMember(message model.ReceivedMessage) {\n\tdata := (*message.Message).(map[string]any)\n\tvalue := data[\"value\"].(bool)\n\n\tuser, err := client.hub.guildService.GetVCMember(client.ID, message.Room)\n\n\tif err != nil {\n\t\tlog.Printf(\"could not find vc member: %v\", err)\n\t\treturn\n\t}\n\n\tif message.Action == ToggleMute {\n\t\tuser.IsMuted = value\n\t} else if message.Action == ToggleDeafen {\n\t\tuser.IsDeafened = value\n\t}\n\n\terr = client.hub.guildService.UpdateVCMember(user.IsMuted, user.IsDeafened, client.ID, message.Room)\n\n\tif err != nil {\n\t\tlog.Printf(\"could not update vc member: %v\", err)\n\t\treturn\n\t}\n\n\tif room := client.hub.findRoomById(message.Room); room != nil {\n\t\tmsg := model.WebsocketMessage{\n\t\t\tAction: message.Action,\n\t\t\tData:   message.Message,\n\t\t}\n\t\troom.broadcast <- &msg\n\t}\n}\n\n// isMember checks if the user is member of the given guild\nfunc isMember(guild *model.Guild, userId string) bool {\n\tfor _, v := range guild.Members {\n\t\tif v.ID == userId {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/ws/hub.go",
    "content": "package ws\n\nimport (\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/sentrionic/valkyrie/model\"\n)\n\n// Hub contains all rooms and clients\ntype Hub struct {\n\tclients        map[*Client]bool\n\tregister       chan *Client\n\tunregister     chan *Client\n\tbroadcast      chan []byte\n\trooms          map[*Room]bool\n\tchannelService model.ChannelService\n\tguildService   model.GuildService\n\tuserService    model.UserService\n\tredisClient    *redis.Client\n}\n\n// Config will hold services that will eventually be injected into this\n// service layer\ntype Config struct {\n\tUserService    model.UserService\n\tGuildService   model.GuildService\n\tChannelService model.ChannelService\n\tRedis          *redis.Client\n}\n\n// NewWebsocketHub creates a new Hub\nfunc NewWebsocketHub(c *Config) *Hub {\n\treturn &Hub{\n\t\tclients:        make(map[*Client]bool),\n\t\tregister:       make(chan *Client),\n\t\tunregister:     make(chan *Client),\n\t\tbroadcast:      make(chan []byte),\n\t\trooms:          make(map[*Room]bool),\n\t\tchannelService: c.ChannelService,\n\t\tguildService:   c.GuildService,\n\t\tuserService:    c.UserService,\n\t\tredisClient:    c.Redis,\n\t}\n}\n\n// Run our websocket server, accepting various requests\nfunc (hub *Hub) Run() {\n\tfor {\n\t\tselect {\n\n\t\tcase client := <-hub.register:\n\t\t\thub.registerClient(client)\n\n\t\tcase client := <-hub.unregister:\n\t\t\thub.unregisterClient(client)\n\n\t\tcase message := <-hub.broadcast:\n\t\t\thub.broadcastToClients(message)\n\t\t}\n\t}\n}\n\nfunc (hub *Hub) registerClient(client *Client) {\n\thub.clients[client] = true\n}\n\nfunc (hub *Hub) unregisterClient(client *Client) {\n\tdelete(hub.clients, client)\n}\n\nfunc (hub *Hub) broadcastToClients(message []byte) {\n\tfor client := range hub.clients {\n\t\tclient.send <- message\n\t}\n}\n\n// BroadcastToRoom sends the given message to all clients connected to the given room\nfunc (hub *Hub) BroadcastToRoom(message []byte, roomId string) {\n\tif room := hub.findRoomById(roomId); room != nil {\n\t\troom.publishRoomMessage(message)\n\t}\n}\n\nfunc (hub *Hub) findRoomById(id string) *Room {\n\tvar foundRoom *Room\n\tfor room := range hub.rooms {\n\t\tif room.GetId() == id {\n\t\t\tfoundRoom = room\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn foundRoom\n}\n\nfunc (hub *Hub) createRoom(id string) *Room {\n\troom := NewRoom(id, hub.redisClient)\n\tgo room.RunRoom()\n\thub.rooms[room] = true\n\n\treturn room\n}\n"
  },
  {
    "path": "server/ws/room.go",
    "content": "package ws\n\nimport (\n\t\"context\"\n\t\"github.com/redis/go-redis/v9\"\n\t\"github.com/sentrionic/valkyrie/model\"\n\t\"log\"\n)\n\n// Room represents a websocket room\ntype Room struct {\n\tid         string\n\tclients    map[*Client]bool\n\tregister   chan *Client\n\tunregister chan *Client\n\tbroadcast  chan *model.WebsocketMessage\n\tredis      *redis.Client\n}\n\nvar ctx = context.Background()\n\n// NewRoom creates a new Room\nfunc NewRoom(id string, rds *redis.Client) *Room {\n\treturn &Room{\n\t\tid:         id,\n\t\tclients:    make(map[*Client]bool),\n\t\tregister:   make(chan *Client),\n\t\tunregister: make(chan *Client),\n\t\tbroadcast:  make(chan *model.WebsocketMessage),\n\t\tredis:      rds,\n\t}\n}\n\n// RunRoom runs our room, accepting various requests\nfunc (room *Room) RunRoom() {\n\n\tgo room.subscribeToRoomMessages()\n\n\tfor {\n\t\tselect {\n\n\t\tcase client := <-room.register:\n\t\t\troom.registerClientInRoom(client)\n\n\t\tcase client := <-room.unregister:\n\t\t\troom.unregisterClientInRoom(client)\n\n\t\tcase message := <-room.broadcast:\n\t\t\troom.publishRoomMessage(message.Encode())\n\t\t}\n\t}\n}\n\n// registerClientInRoom adds the client to the room\nfunc (room *Room) registerClientInRoom(client *Client) {\n\troom.clients[client] = true\n}\n\n// unregisterClientInRoom removes the client from the room\nfunc (room *Room) unregisterClientInRoom(client *Client) {\n\tdelete(room.clients, client)\n}\n\n// broadcastToClientsInRoom sends the given message to all members in the room\nfunc (room *Room) broadcastToClientsInRoom(message []byte) {\n\tfor client := range room.clients {\n\t\tclient.send <- message\n\t}\n}\n\n// GetId returns the ID of the room\nfunc (room *Room) GetId() string {\n\treturn room.id\n}\n\n// publishRoomMessage publishes the message to all clients subscribing to the room\nfunc (room *Room) publishRoomMessage(message []byte) {\n\terr := room.redis.Publish(ctx, room.GetId(), message).Err()\n\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n}\n\n// subscribeToRoomMessages subscribes to messages in this room\nfunc (room *Room) subscribeToRoomMessages() {\n\tpubsub := room.redis.Subscribe(ctx, room.GetId())\n\n\tch := pubsub.Channel()\n\n\tfor msg := range ch {\n\t\troom.broadcastToClientsInRoom([]byte(msg.Payload))\n\t}\n}\n"
  },
  {
    "path": "web/.eslintrc.json",
    "content": "{\n  \"env\": {\n    \"browser\": true,\n    \"es2021\": true\n  },\n  \"extends\": [\"plugin:react/recommended\", \"airbnb\", \"airbnb-typescript\", \"prettier\"],\n  \"parser\": \"@typescript-eslint/parser\",\n  \"parserOptions\": {\n    \"ecmaFeatures\": {\n      \"jsx\": true\n    },\n    \"ecmaVersion\": 12,\n    \"sourceType\": \"module\",\n    \"project\": \"./tsconfig.json\"\n  },\n  \"plugins\": [\"react\", \"@typescript-eslint\", \"react-hooks\", \"prettier\"],\n  \"rules\": {\n    \"linebreak-style\": 0,\n    \"no-undef\": \"off\",\n    \"no-empty\": \"off\",\n    \"no-use-before-define\": \"off\",\n    \"@typescript-eslint/no-use-before-define\": [\"off\"],\n    \"react/jsx-filename-extension\": [\n      \"warn\",\n      {\n        \"extensions\": [\".tsx\"]\n      }\n    ],\n    \"import/extensions\": [\n      \"error\",\n      \"ignorePackages\",\n      {\n        \"ts\": \"never\",\n        \"tsx\": \"never\"\n      }\n    ],\n    \"@typescript-eslint/explicit-function-return-type\": [\n      \"error\",\n      {\n        \"allowExpressions\": true\n      }\n    ],\n    \"no-unused-vars\": \"off\",\n    \"@typescript-eslint/no-unused-vars\": [\n      \"error\", // or error\n      {\n        \"argsIgnorePattern\": \"^_\",\n        \"varsIgnorePattern\": \"^_\",\n        \"caughtErrorsIgnorePattern\": \"^_\"\n      }\n    ],\n    \"react/jsx-no-useless-fragment\": \"off\",\n    \"react-hooks/rules-of-hooks\": \"error\",\n    \"react-hooks/exhaustive-deps\": \"warn\",\n    \"react/require-default-props\": \"off\",\n    \"react/prop-types\": \"off\",\n    \"import/prefer-default-export\": \"off\",\n    \"max-len\": [\n      \"error\",\n      {\n        \"code\": 120\n      }\n    ],\n    \"react/function-component-definition\": [\n      2,\n      {\n        \"namedComponents\": \"arrow-function\",\n        \"unnamedComponents\": \"arrow-function\"\n      }\n    ]\n  },\n  \"settings\": {\n    \"import/resolver\": {\n      \"typescript\": {}\n    }\n  },\n  \"ignorePatterns\": [\"*.css\", \"*.html\"]\n}\n"
  },
  {
    "path": "web/.gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.development\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n.eslintcache\n\n/cypress/downloads\n"
  },
  {
    "path": "web/.prettierrc",
    "content": "{\n  \"printWidth\": 120,\n  \"semi\": true,\n  \"tabWidth\": 2,\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "web/cypress/e2e/account.cy.ts",
    "content": "import { uuid } from '../support/utils';\n\n// Covers the account routes\ndescribe('Account related pages', () => {\n  const id = uuid();\n  const email = `${id}@example.com`;\n\n  it('redirects an unauthenticated user', () => {\n    cy.visit('/channels/me');\n    cy.url().should('include', '/login');\n  });\n\n  it('registers the user', () => {\n    cy.registerUser(email, id);\n  });\n\n  it('signs in the user', () => {\n    cy.loginUser(email);\n  });\n\n  it(\"checks the user's settings page\", () => {\n    cy.loginUser(email);\n    cy.get('[aria-label=settings]').click();\n\n    // Confirm account page got user's info\n    cy.url().should('include', '/account');\n    cy.contains('My Account'.toUpperCase());\n    cy.get('input[name=\"email\"]').should('have.value', email);\n    cy.get('input[name=\"username\"]').should('have.value', id);\n  });\n\n  it(\"updates the user's info\", () => {\n    cy.loginUser(email);\n    cy.get('[aria-label=settings]').click();\n\n    // Update the user's info and check for confirmation toast\n    cy.url().should('include', '/account');\n    cy.contains('My Account'.toUpperCase());\n    cy.get('input[name=\"email\"]').should('have.value', email);\n    cy.get('input[name=\"username\"]').clear().type('Test').should('have.value', 'Test');\n    cy.get('[type=submit]').click();\n\n    cy.wait(200);\n    cy.contains('Account Updated.');\n  });\n\n  it(\"updates the user's password\", () => {\n    cy.loginUser(email);\n    cy.get('[aria-label=settings]').click();\n\n    cy.url().should('include', '/account');\n    cy.contains('Change Password').click();\n\n    // Change the user's password\n    cy.contains('Change your password');\n    cy.get('input[name=\"currentPassword\"]').type('password').should('have.value', 'password');\n    cy.get('input[name=\"newPassword\"]').type('password').should('have.value', 'password');\n    cy.get('input[name=\"confirmNewPassword\"]').type('password').should('have.value', 'password');\n    cy.contains('Done').click();\n\n    cy.wait(200);\n    cy.contains('Changed Password');\n  });\n\n  it('signs out the user', () => {\n    cy.loginUser(email);\n    cy.get('[aria-label=settings]').click();\n    cy.url().should('include', '/account');\n\n    cy.contains('Logout').click();\n\n    cy.url().should('include', '');\n  });\n});\n"
  },
  {
    "path": "web/cypress/e2e/channel.cy.ts",
    "content": "import { uuid } from '../support/utils';\n\ndescribe('Channels related actions', () => {\n  // The user's values\n  const id = uuid().toString();\n  const email = `${id}@example.com`;\n  let guildId = '';\n  let channelId = '';\n\n  it('registers the user', () => {\n    cy.registerUser(email, id);\n  });\n\n  it('creates a guild', () => {\n    cy.loginUser(email);\n\n    cy.intercept({\n      method: 'POST',\n      pathname: '/api/guilds/create',\n    }).as('create');\n\n    cy.createGuild(id);\n\n    // Confirm the user got sent to the guild and default channel\n    cy.wait('@create').then((interception) => {\n      const body = interception.response.body;\n      const url = `channels/${body.id}/${body.default_channel_id}`;\n      cy.url().should('include', url);\n      guildId = body.id;\n    });\n  });\n\n  it('creates a channel for the guild', () => {\n    cy.loginUser(email);\n\n    cy.clickOnFirstGuild();\n    cy.openGuildMenu();\n\n    cy.intercept({\n      method: 'POST',\n      pathname: `/api/channels/${guildId}`,\n    }).as('create');\n\n    cy.contains('Create Channel').click();\n    cy.get('input[name=\"name\"]').type('random').should('have.value', 'random');\n    cy.get('[type=submit]').click();\n\n    // Confirm the user got sent to the newly created channel\n    cy.wait('@create').then((interception) => {\n      const body = interception.response.body;\n      channelId = body.id;\n      const url = `channels/${guildId}/${channelId}`;\n      cy.url().should('include', url);\n      cy.contains('random').should('exist');\n    });\n  });\n\n  it('should successfully switch between channels', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n    cy.contains('Welcome to #general').should('exist');\n\n    // Check the other channel is the correct one\n    cy.get(`a[href^=\"/channels/${guildId}/\"]`).last().click();\n    cy.contains('Welcome to #random').should('exist');\n  });\n\n  it('should successfully edit the channel', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.contains('random').trigger('mouseover');\n    cy.get('[aria-label=\"edit channel\"]').click();\n\n    cy.intercept({\n      method: 'PUT',\n      pathname: `/api/channels/${channelId}`,\n    }).as('update');\n\n    // Edit the values\n    cy.get('input[name=\"name\"]').clear().type('secret').should('have.value', 'secret');\n    cy.get('input[type=\"checkbox\"]').check({ force: true });\n    cy.get('[type=submit]').click();\n\n    // Check that the edited channel exists\n    cy.wait('@update').then((_) => {\n      cy.contains('secret').should('exist');\n    });\n  });\n\n  it('should successfully delete the channel', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.get(`a[href^=\"/channels/${guildId}/\"]`).last().click();\n    cy.contains('secret').last().trigger('mouseover');\n    cy.get('[aria-label=\"edit channel\"]').last().click();\n\n    cy.intercept({\n      method: 'DELETE',\n      pathname: `/api/channels/${channelId}`,\n    }).as('delete');\n\n    cy.contains('Delete Channel').click();\n    cy.get('button').contains('Delete Channel').click();\n\n    cy.wait('@delete').then((interception) => {\n      // Check that the channel is gone and that the user got moved\n      cy.contains('secret').should('have.length.lte', 1);\n      cy.contains('Welcome to #general').should('exist');\n    });\n  });\n});\n"
  },
  {
    "path": "web/cypress/e2e/friend.cy.ts",
    "content": "import { uuid } from '../support/utils';\n\ndescribe('Friends related actions', () => {\n  // Friend values\n  const friendName = uuid();\n  const friendEmail = `${friendName}@example.com`;\n  let friendId = '';\n\n  // AuthUser values\n  const authName = uuid();\n  const email = `${authName}@example.com`;\n\n  it('registers the friend and gets the id', () => {\n    cy.registerUser(friendEmail, friendName);\n\n    // Get the friend's ID and store it\n    cy.contains('Add Friend').click();\n    cy.get('input')\n      .first()\n      .invoke('val')\n      .then((text) => {\n        friendId = text.toString();\n      });\n  });\n\n  it('signs up the user and gets the id', () => {\n    cy.registerUser(email, authName);\n  });\n\n  it('sends a friend request to the member', () => {\n    cy.loginUser(email);\n    cy.sendRequest(friendId);\n\n    // Confirm the 'Pending' tab got an outgoing friend request\n    cy.contains('Pending').click();\n    cy.contains(friendName);\n    cy.contains('Outgoing Friend Request');\n  });\n\n  it('cancels the outgoing request', () => {\n    cy.loginUser(email);\n\n    cy.contains('Pending').click();\n    cy.contains(friendName);\n    // Confirm the accept button does not exist\n    cy.get('[aria-label=\"accept request\"]').should('not.exist');\n    cy.get('[aria-label=\"decline request\"]').click();\n\n    // Confirm the user got removed and is not a friend.\n    cy.contains(friendName).should('not.exist');\n    cy.get('button').contains('Friends').click();\n    cy.contains(friendName).should('not.exist');\n  });\n\n  it('sends another friend request to the member to be checked', () => {\n    cy.loginUser(email);\n    cy.sendRequest(friendId);\n  });\n\n  it('checks the incoming friend request and declines it', () => {\n    cy.loginUser(friendEmail);\n\n    // Confirm the user got a request and has the accept button\n    cy.contains('Pending').click();\n    cy.contains(authName);\n    cy.contains('Incoming Friend Request');\n    cy.get('[aria-label=\"accept request\"]').should('exist');\n\n    // Decline request\n    cy.get('[aria-label=\"decline request\"]').click();\n    cy.contains(authName).should('not.exist');\n\n    // Confirm the user got removed and is not a friend.\n    cy.get('button').contains('Friends').click();\n    cy.wait(100);\n    cy.contains(authName).should('not.exist');\n  });\n\n  it('sends another friend request to the member to be accepted', () => {\n    cy.loginUser(email);\n    cy.sendRequest(friendId);\n  });\n\n  it('accepts the friend request', () => {\n    cy.loginUser(friendEmail);\n\n    // Accept request\n    cy.contains('Pending').click();\n    cy.contains(authName);\n    cy.get('[aria-label=\"accept request\"]').click();\n    cy.contains(authName).should('not.exist');\n\n    // Confirm the user is in the 'Friends' tab\n    cy.get('button').contains('Friends').click();\n    cy.wait(100);\n    cy.contains(authName).should('exist');\n  });\n\n  it('directs to the friends dms when clicked', () => {\n    cy.loginUser(email);\n\n    cy.intercept({\n      method: 'POST',\n      pathname: `/api/channels/${friendId}/dm`,\n    }).as('getDM');\n\n    // Open the DM\n    cy.contains(friendName).should('exist').click();\n\n    // Confirm it's the friend's DM\n    cy.contains(friendName).should('exist');\n    cy.contains(`This is the beginning of your direct message history with @${friendName}`).should('exist');\n    cy.get('textarea[name=\"text\"]').invoke('attr', 'placeholder').should('contain', `@${friendName}`);\n\n    // Confirm the DM's url is the correct one\n    cy.wait('@getDM').then((interception) => {\n      const body = interception.response.body;\n      const url = `/channels/me/${body.id}`;\n      cy.url().should('include', url);\n    });\n  });\n\n  it('should successfully go to the DM when clicked on the item', () => {\n    cy.loginUser(email);\n    cy.get('ul[id=\"dm-list\"]').children().contains(friendName).click();\n\n    // Confirm it's the friend's DM\n    cy.contains(friendName).should('exist');\n    cy.contains(`This is the beginning of your direct message history with @${friendName}`).should('exist');\n    cy.get('textarea[name=\"text\"]').invoke('attr', 'placeholder').should('contain', `@${friendName}`);\n\n    // Check that messaging is possible and the message gets added to the chat\n    cy.get('textarea[name=\"text\"]').type('Hello World{enter}').should('have.value', '');\n    cy.wait(50);\n    cy.contains('Hello World');\n  });\n\n  it('closes the dm when the close button is pressed', () => {\n    cy.loginUser(email);\n\n    // Check that the DM exists\n    cy.get('ul[id=\"dm-list\"]').children().contains(friendName).should('exist');\n    cy.get('ul[id=\"dm-list\"] li:first').trigger('mouseover');\n\n    // Close the DM and confirm it's gone\n    cy.get('[aria-label=\"close dm\"]').click();\n    cy.get('ul[id=\"dm-list\"]').children().contains(friendName).should('not.exist');\n  });\n\n  it('removes the friend', () => {\n    cy.loginUser(email);\n\n    // Check that the friend exists in the tab\n    cy.get('ul[id=\"friend-list\"]').children().contains(friendName).should('exist');\n\n    // Confirm and remove friend\n    cy.get('[aria-label=\"remove friend\"]').click();\n    cy.contains('Remove Friend').click();\n\n    // Confirm the friend got removed\n    cy.get('ul[id=\"friend-list\"]').should('not.exist');\n  });\n});\n"
  },
  {
    "path": "web/cypress/e2e/guild.cy.ts",
    "content": "import { uuid } from '../support/utils';\n\ndescribe('Guild related actions', () => {\n  // The user's values\n  const id = uuid().toString();\n  const email = `${id}@example.com`;\n\n  // The mock members values\n  const memberId = uuid().toString();\n  const memberEmail = `${memberId}@example.com`;\n\n  // The invite link and guildId\n  let invite = '';\n  let guildId = '';\n\n  it('registers the user', () => {\n    cy.registerUser(email, id);\n  });\n\n  it('creates a guild', () => {\n    cy.loginUser(email);\n\n    cy.intercept({\n      method: 'POST',\n      pathname: '/api/guilds/create',\n    }).as('create');\n\n    cy.createGuild(id);\n\n    // Confirm the user got sent to the guild and default channel\n    cy.wait('@create').then((interception) => {\n      const body = interception.response.body;\n      const url = `channels/${body.id}/${body.default_channel_id}`;\n      cy.url().should('include', url);\n      guildId = body.id;\n    });\n  });\n\n  it('should update the server after it got edited', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.openGuildMenu();\n    cy.contains('Server Settings').click();\n\n    cy.intercept({\n      method: 'PUT',\n      pathname: `/api/guilds/${guildId}`,\n    }).as('update');\n\n    // Updates the values and saves them\n    cy.get('input[name=\"name\"]').clear().type('Valkyrie').should('have.value', 'Valkyrie');\n    cy.contains('Save Changes').click();\n\n    // Check the updates were applied\n    cy.wait('@update').then((_) => {\n      cy.contains('Valkyrie').should('exist');\n    });\n  });\n\n  it('should successfully clear the invites', () => {\n    cy.loginUser(email);\n\n    cy.clickOnFirstGuild();\n    cy.openGuildMenu();\n    cy.contains('Server Settings').click();\n\n    cy.intercept({\n      method: 'DELETE',\n      pathname: `/api/guilds/${guildId}/invite`,\n    }).as('clear');\n\n    cy.contains('Invalidate Links').click();\n\n    // Check the invites succesfully got cleared\n    cy.wait('@clear').then((interception) => {\n      const body = interception.response.body;\n      expect(body).eq(true);\n    });\n  });\n\n  it('should delete the server and go to home', () => {\n    cy.loginUser(email);\n\n    cy.clickOnFirstGuild();\n\n    // Delete server and confirm the request\n    cy.openGuildMenu();\n    cy.contains('Server Settings').click();\n    cy.contains('Delete Server').click();\n    cy.contains('Delete Server').click();\n\n    // Confirm the user is back at home and the guild is gone\n    // Not working due to websocket connection problems\n    // cy.url().should('include', '/channels/me');\n    // cy.get('a[href*=\"channels\"]').not('a[href*=\"channels/me\"]').should('have.length', 0);\n  });\n\n  it('creates another guild to get an invite', () => {\n    cy.loginUser(email);\n\n    cy.intercept({\n      method: 'POST',\n      pathname: '/api/guilds/create',\n    }).as('create');\n\n    cy.createGuild(id);\n\n    // Confirm the user got sent to the guild and default channel\n    cy.wait('@create').then((interception) => {\n      const body = interception.response.body;\n      const url = `channels/${body.id}/${body.default_channel_id}`;\n      cy.url().should('include', url);\n      guildId = body.id;\n    });\n\n    // get the invite for the other user\n    cy.openGuildMenu();\n    cy.contains('Invite People').click();\n\n    // Check unlimited invites\n    cy.get('input[type=\"checkbox\"]').check({ force: true });\n    cy.wait(50);\n\n    // Store the invite in the variable\n    cy.get('input[id=\"invite-link\"]')\n      .first()\n      .invoke('val')\n      .then((text) => {\n        invite = text.toString();\n      });\n  });\n\n  it('joins the guild for the given link', () => {\n    cy.registerUser(memberEmail, memberId);\n\n    cy.intercept({\n      method: 'POST',\n      pathname: '/api/guilds/join',\n    }).as('join');\n\n    cy.joinGuild(invite);\n\n    // Confirm the user got sent to the guild and default channel\n    cy.wait('@join').then((interception) => {\n      const body = interception.response.body;\n      const url = `channels/${body.id}/${body.default_channel_id}`;\n      cy.url().should('include', url);\n    });\n\n    cy.contains(`${id}'s server`).should('exist');\n    cy.contains(`Welcome to #general`).should('exist');\n  });\n\n  it('should not show \"Server Settings\" and \"Create Channel\" to the non owner', () => {\n    cy.loginUser(memberEmail);\n\n    cy.clickOnFirstGuild();\n\n    cy.openGuildMenu();\n    cy.contains('Server Settings').should('not.exist');\n    cy.contains('Create Channel').should('not.exist');\n  });\n\n  it('should leave the server', () => {\n    cy.loginUser(memberEmail);\n\n    cy.clickOnFirstGuild();\n\n    cy.openGuildMenu();\n    cy.contains('Leave Server').click();\n\n    // Confirm the user is back at home and the guild is gone\n    cy.url().should('include', '/channels/me');\n    cy.get('a[href*=\"channels\"]').not('a[href*=\"channels/me\"]').should('have.length', 0);\n  });\n\n  it('creates a third guild to test switching', () => {\n    cy.loginUser(email);\n    cy.createGuild('Test');\n  });\n\n  it('successfully switches between the guilds', () => {\n    cy.loginUser(email);\n\n    cy.clickOnFirstGuild();\n    cy.url().should('include', guildId);\n    cy.contains(`${id}'s server`).should('exist');\n\n    // Click on the second server and confirm it's the second one created\n    cy.get('a[href*=\"channels\"]').eq(2).click();\n    cy.contains(`Test's server`).should('exist');\n  });\n});\n"
  },
  {
    "path": "web/cypress/e2e/member.cy.ts",
    "content": "import { uuid } from '../support/utils';\n\ndescribe('Members related actions', () => {\n  // Member values\n  const memberName = uuid();\n  const memberEmail = `${memberName}@example.com`;\n  let invite = '';\n\n  // AuthUser values\n  const authName = uuid();\n  const email = `${authName}@example.com`;\n\n  it('registers the user', () => {\n    cy.registerUser(email, authName);\n  });\n\n  it('creates a guild and get the invite', () => {\n    cy.loginUser(email);\n    cy.createGuild(authName);\n\n    // get the invite for the other user\n    cy.openGuildMenu();\n    cy.contains('Invite People').click();\n\n    // Check unlimited invites\n    cy.get('input[type=\"checkbox\"]').check({ force: true });\n    cy.wait(50);\n\n    // Store the invite in the variable\n    cy.get('input[id=\"invite-link\"]')\n      .first()\n      .invoke('val')\n      .then((text) => {\n        invite = text.toString();\n      });\n  });\n\n  it('successfully changes the members settings', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.openGuildMenu();\n    cy.contains('Change Appearance').click();\n\n    // Sets the member values\n    cy.get('input[name=\"nickname\"]').clear().type('Tester').should('have.value', 'Tester');\n    cy.get('div[title=\"#0693E3\"]').click();\n    cy.wait(100);\n\n    cy.contains('Save').click();\n  });\n\n  it('joins the guild for the given link', () => {\n    cy.registerUser(memberEmail, memberName);\n    cy.joinGuild(invite);\n\n    // Confirm the above changes\n    cy.wait(100);\n    cy.contains('Tester').should('exist');\n\n    // Check that the two members + two labels are there\n    cy.get('ul[id=\"member-list\"]').children().should('have.length', 4);\n  });\n\n  it('should not show \"Kick\" and \"Ban\" options to the non owner', () => {\n    cy.loginUser(memberEmail);\n    cy.clickOnFirstGuild();\n\n    cy.get('ul[id=\"member-list\"]').contains('Tester').rightclick();\n    cy.contains('Ban').should('not.exist');\n    cy.contains('Kick').should('not.exist');\n  });\n\n  it('successfully resets the members settings', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.openGuildMenu();\n    cy.contains('Change Appearance').click();\n\n    // Reset the values\n    cy.contains('Reset Nickname').click();\n    cy.contains('Reset Color').click();\n    cy.wait(100);\n\n    cy.contains('Save').click();\n  });\n\n  it('should go to the members DMs on click', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.contains(memberName).rightclick();\n    cy.contains('Message').click();\n\n    // Confirm the user got moved to the correct DM\n    cy.contains(memberName).should('exist');\n    cy.contains(`This is the beginning of your direct message history with @${memberName}`).should('exist');\n    cy.get('textarea[name=\"text\"]').invoke('attr', 'placeholder').should('contain', `@${memberName}`);\n  });\n\n  it('should successfully sent a friends request', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.contains(memberName).rightclick();\n    cy.contains('Add Friend').click();\n\n    // Go to the pending tab to confirm the request\n    cy.get('a[href=\"/channels/me\"]').click();\n    cy.contains('Pending').click();\n    cy.contains(memberName).should('exist');\n    cy.contains('Outgoing Friend Request').should('exist');\n  });\n\n  it('should kick and remove the member', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Kick the user\n    cy.contains(memberName).rightclick();\n    cy.contains(`Kick ${memberName}`).click();\n    cy.get('button').contains('Kick').click();\n\n    // Confirm the member is gone\n    cy.get('ul[id=\"member-list\"]').children().should('have.length', 3);\n    cy.contains(memberName).should('not.exist');\n  });\n\n  it('should successfully rejoin the server', () => {\n    cy.loginUser(memberEmail);\n    cy.joinGuild(invite);\n  });\n\n  it('should ban and remove the member', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Ban the user\n    cy.contains(memberName).rightclick();\n    cy.contains(`Ban ${memberName}`).click();\n    cy.get('button').contains('Ban').click();\n\n    // Confirm the member is gone\n    cy.get('ul[id=\"member-list\"]').children().should('have.length', 3);\n    cy.contains(memberName).should('not.exist');\n  });\n\n  it('should not be able to rejoin the server', () => {\n    cy.loginUser(memberEmail);\n    cy.joinGuild(invite);\n\n    // Confirm the user did not join the guild\n    cy.contains('You are banned from this server').should('exist');\n    cy.url().should('include', '/channels/me');\n  });\n\n  it('should contain the member in the ban list', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Go to the ban list modal\n    cy.openGuildMenu();\n    cy.contains('Server Settings').click();\n    cy.contains('Bans').click();\n\n    // Check the member exists there\n    cy.wait(100);\n    cy.contains(memberName).should('exist');\n    cy.get('button[aria-label=\"unban user\"]').click();\n\n    // Confirm the member got unbanned\n    cy.contains(memberName).should('not.exist');\n  });\n\n  it('should not display a context menu when clicking on oneself', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.contains(authName).rightclick();\n    cy.contains('Add Friend').should('not.exist');\n  });\n\n  it('should toggle the member list on click', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Toggle the member list\n    cy.get('ul[id=\"member-list\"]').should('be.visible');\n    cy.get('[aria-label=\"toggle member list\"]').click();\n    cy.get('ul[id=\"member-list\"]').should('not.exist');\n    cy.get('[aria-label=\"toggle member list\"]').click();\n    cy.get('ul[id=\"member-list\"]').should('be.visible');\n  });\n});\n"
  },
  {
    "path": "web/cypress/e2e/message.cy.ts",
    "content": "import { uuid } from '../support/utils';\n\nconst waitTime = 1000;\n\ndescribe('Message related actions', () => {\n  // The user's values\n  const id = uuid().toString();\n  const email = `${id}@example.com`;\n\n  // The mock members values\n  const memberName = uuid().toString();\n  const memberEmail = `${memberName}@example.com`;\n\n  let userId = '';\n\n  // The invite link and guildId\n  let invite = '';\n\n  it('should register the user and create a guild', () => {\n    cy.intercept({\n      method: 'POST',\n      pathname: '/api/account/register',\n    }).as('register');\n\n    cy.registerUser(email, id);\n\n    cy.wait('@register').then((interception) => {\n      const body = interception.response.body;\n      userId = body.id;\n    });\n\n    cy.createGuild(id);\n  });\n\n  it('should successfully post a message', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Send message\n    cy.get('textarea[name=\"text\"]').type('Hello, World{enter}').should('have.value', '');\n\n    // Confirm it got added to the chat\n    // cy.wait(100);\n    // cy.getChat().should('exist');\n    // cy.getChat().children().should('have.length', 1);\n    // cy.firstMessage().contains('Hello, World');\n  });\n\n  it('should confirm that the message got sent', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.wait(waitTime);\n    cy.getChat().should('exist');\n    cy.getChat().children().should('have.length', 1);\n    cy.firstMessage().contains('Hello, World');\n  });\n\n  it('should post another message', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.get('textarea[name=\"text\"]').type('Hello, Server{enter}').should('have.value', '');\n  });\n\n  it('should display newer messages under older', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Confirm it got added under the first one\n    cy.wait(waitTime);\n    cy.getChat().children().should('have.length', 2);\n    cy.firstMessage().contains('Hello, Server');\n    cy.getChat().children().last().contains('Hello, World');\n  });\n\n  it('should successfully delete the message', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.firstMessage().rightclick();\n    cy.contains('Delete Message').click();\n    cy.get('button').contains('Delete').click();\n  });\n\n  // Manually check the message is gone because of websocket problems\n  it('should confirm that the message got deleted', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.wait(waitTime);\n    cy.getChat().children().should('have.length', 1);\n    cy.getChat().children().contains('Hello, Server').should('not.exist');\n  });\n\n  it('should successfully edit the message', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.firstMessage().rightclick();\n    cy.contains('Edit Message').click();\n    cy.wait(waitTime);\n\n    cy.get('input[id=\"editMessage\"]').clear().type('Hello, Update');\n    cy.get('button').contains('Save').click();\n\n    // // Confirm it got edited and got the edit span\n    // cy.firstMessage().contains('Hello, Update');\n    // cy.firstMessage().contains('(edited)');\n  });\n\n  it('should confirm the message got edited', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Confirm it got edited and got the edit span\n    cy.firstMessage().contains('Hello, Update');\n    cy.firstMessage().contains('(edited)');\n  });\n\n  it('should not display \"Add Friend\" or \"Message\" for own avatar', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    cy.wait(waitTime);\n    cy.firstMessage().get('img').eq(1).rightclick();\n    cy.contains('Add Friend').should('not.exist');\n    cy.contains('Message').should('not.exist');\n  });\n\n  it('should get an invite for the other member', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // get the invite for the other user\n    cy.openGuildMenu();\n    cy.contains('Invite People').click();\n\n    // Check unlimited invites\n    cy.get('input[type=\"checkbox\"]').check({ force: true });\n    cy.wait(waitTime);\n\n    // Store the invite in the variable\n    cy.get('input[id=\"invite-link\"]')\n      .first()\n      .invoke('val')\n      .then((text) => {\n        invite = text.toString();\n      });\n  });\n\n  it('should register the other member and join the guild', () => {\n    cy.registerUser(memberEmail, memberName);\n    cy.joinGuild(invite);\n  });\n\n  it('should not display message options for the member context menu when clicking on the owners message', () => {\n    cy.loginUser(memberEmail);\n    cy.clickOnFirstGuild();\n\n    cy.getChat().children().last().rightclick();\n    cy.contains('Edit Message').should('not.exist');\n    cy.contains('Delete Message').should('not.exist');\n  });\n\n  it('should successfully add the user', () => {\n    cy.loginUser(memberEmail);\n    cy.clickOnFirstGuild();\n\n    cy.intercept({\n      method: 'POST',\n      pathname: `/api/account/${userId}/friend`,\n    }).as('addFriend');\n\n    cy.wait(waitTime);\n    cy.firstMessage().get('img').eq(1).rightclick();\n    cy.contains('Add Friend').click();\n\n    cy.wait('@addFriend').then((_) => {\n      // Go to the pending tab to confirm the request\n      cy.get('a[href=\"/channels/me\"]').click();\n      cy.contains('Pending').click();\n      cy.contains(memberName).should('exist');\n      cy.contains('Outgoing Friend Request').should('exist');\n    });\n  });\n\n  it('should successfully go to the DMs with the user', () => {\n    cy.loginUser(memberEmail);\n    cy.clickOnFirstGuild();\n\n    cy.intercept({\n      method: 'POST',\n      pathname: `/api/channels/${userId}/dm`,\n    }).as('create');\n\n    cy.wait(waitTime);\n    cy.firstMessage().get('img').eq(1).rightclick();\n    cy.contains('Message').click();\n\n    cy.wait('@create').then(() => {\n      // Confirm the user got moved to the correct DM\n      cy.contains(id).should('exist');\n      cy.contains(`This is the beginning of your direct message history with @${id}`).should('exist');\n      cy.get('textarea[name=\"text\"]').invoke('attr', 'placeholder').should('contain', `@${id}`);\n    });\n  });\n\n  it('should successfully post a message', () => {\n    cy.loginUser(memberEmail);\n    cy.clickOnFirstGuild();\n\n    cy.get('textarea[name=\"text\"]').type('Hello, Owner{enter}').should('have.value', '');\n  });\n\n  it(\"should be able to delete the member's message as the owner\", () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Owner can delete the other person's message, but cannot edit it\n    cy.firstMessage().rightclick();\n    cy.contains('Edit Message').should('not.exist');\n\n    cy.contains('Delete Message').click();\n    cy.get('button').contains('Delete').click();\n  });\n\n  it('should confirm that the message got deleted', () => {\n    cy.loginUser(email);\n    cy.clickOnFirstGuild();\n\n    // Confirm the message got deleted\n    cy.wait(100);\n    cy.getChat().children().should('have.length', 1);\n    cy.getChat().children().contains('Hello, Owner').should('not.exist');\n  });\n});\n"
  },
  {
    "path": "web/cypress/fixtures/example.json",
    "content": "{\n  \"name\": \"Using fixtures to represent data\",\n  \"email\": \"hello@cypress.io\",\n  \"body\": \"Fixtures are a great way to mock data for responses to routes\"\n}\n"
  },
  {
    "path": "web/cypress/plugins/index.js",
    "content": "/// <reference types=\"cypress\" />\n// ***********************************************************\n// This example plugins/index.js can be used to load plugins\n//\n// You can change the location of this file or turn off loading\n// the plugins file with the 'pluginsFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/plugins-guide\n// ***********************************************************\n\n// This function is called when a project is opened or re-opened (e.g. due to\n// the project's config changing)\n\n/**\n * @type {Cypress.PluginConfig}\n */\n// eslint-disable-next-line no-unused-vars\nmodule.exports = (on, config) => {\n  // `on` is used to hook into various events Cypress emits\n  // `config` is the resolved Cypress config\n};\n"
  },
  {
    "path": "web/cypress/support/commands.ts",
    "content": "Cypress.Commands.add('registerUser', (email, id) => {\n  cy.visit('/');\n  cy.contains('Valkyrie');\n  cy.contains('Register').click();\n\n  cy.url().should('include', '/register');\n  cy.get('input[name=\"email\"]').type(email).should('have.value', email);\n  cy.get('input[name=\"username\"]').type(id).should('have.value', id);\n  cy.get('input[name=\"password\"]').type('password').should('have.value', 'password');\n  cy.contains('Register').click();\n\n  cy.url().should('include', '/channels/me');\n});\n\nCypress.Commands.add('loginUser', (email) => {\n  cy.visit('/');\n  cy.contains('Valkyrie');\n  cy.contains('Login').click();\n\n  cy.url().should('include', '/login');\n  cy.get('input[name=\"email\"]').type(email).should('have.value', email);\n  cy.get('input[name=\"password\"]').type('password').should('have.value', 'password');\n  cy.contains('Login').click();\n\n  cy.url().should('include', '/channels/me');\n});\n\nCypress.Commands.add('sendRequest', (id) => {\n  cy.contains('Add Friend').click();\n  cy.get('input[name=id]').type(id).should('have.value', id);\n  cy.get('[type=submit]').click();\n});\n\nCypress.Commands.add('createGuild', (id) => {\n  cy.get('[id=\"add-guild-icon\"]').click();\n\n  cy.contains('Create My Own').click();\n  cy.get('input').clear().type(`${id}'s server`);\n  cy.get('[type=submit]').click();\n\n  cy.contains(`${id}'s server`).should('exist');\n  cy.contains(`Welcome to #general`).should('exist');\n});\n\nCypress.Commands.add('clickOnFirstGuild', () => {\n  cy.wait(1000);\n  cy.get('a[href*=\"channels\"]').not('a[href*=\"channels/me\"]').first().click();\n});\n\nCypress.Commands.add('openGuildMenu', () => {\n  cy.get('[id^=\"menu-button-\"]').click();\n});\n\nCypress.Commands.add('joinGuild', (invite) => {\n  cy.get('[id=\"add-guild-icon\"]').click();\n  cy.contains('Join a Server').click();\n\n  cy.get('input').type(invite).should('have.value', invite);\n  cy.get('[type=submit]').click();\n  cy.wait(1000);\n});\n\nCypress.Commands.add('getChat', () => {\n  cy.get('.infinite-scroll-component');\n});\n\nCypress.Commands.add('firstMessage', () => {\n  cy.getChat().children().first();\n});\n"
  },
  {
    "path": "web/cypress/support/e2e.js",
    "content": "// ***********************************************************\n// This example support/index.js is processed and\n// loaded automatically before your test files.\n//\n// This is a great place to put global configuration and\n// behavior that modifies Cypress.\n//\n// You can change the location of this file or turn off\n// automatically serving support files with the\n// 'supportFile' configuration option.\n//\n// You can read more here:\n// https://on.cypress.io/configuration\n// ***********************************************************\n\n// Import commands.js using ES2015 syntax:\nimport './commands';\n\n// Alternatively you can use CommonJS syntax:\n// require('./commands')\n"
  },
  {
    "path": "web/cypress/support/index.d.ts",
    "content": "// load type definitions that come with Cypress module\n// eslint-disable-next-line spaced-comment\n/// <reference types=\"cypress\" />\n\ndeclare namespace Cypress {\n  interface Chainable {\n    registerUser(email: string, id: string): Chainable<AUTWindow>;\n    loginUser(email: string): Chainable<AUTWindow>;\n    sendRequest(id: string): Chainable<AUTWindow>;\n    createGuild(id: string): Chainable<AUTWindow>;\n    joinGuild(invite: string): Chainable<AUTWindow>;\n    clickOnFirstGuild(): Chainable<AUTWindow>;\n    openGuildMenu(): Chainable<AUTWindow>;\n    getChat(): Chainable<AUTWindow>;\n    firstMessage(): Chainable<AUTWindow>;\n  }\n}\n"
  },
  {
    "path": "web/cypress/support/utils.ts",
    "content": "export const uuid = (): string => Cypress._.random(0, 1e6).toString();\n"
  },
  {
    "path": "web/cypress/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\"es5\", \"dom\"],\n    \"types\": [\"cypress\"]\n  },\n  \"include\": [\"**/*.ts\", \"support/index.js\"]\n}\n"
  },
  {
    "path": "web/cypress.config.ts",
    "content": "import { defineConfig } from 'cypress';\n\nexport default defineConfig({\n  video: false,\n  screenshotOnRunFailure: false,\n  retries: 2,\n  e2e: {\n    baseUrl: 'http://localhost:3000',\n  },\n  defaultCommandTimeout: 8000,\n});\n"
  },
  {
    "path": "web/package.json",
    "content": "{\n  \"name\": \"valkyrie\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test\",\n    \"eject\": \"react-scripts eject\",\n    \"lint\": \"npx eslint src/**\",\n    \"prettier\": \"npx prettier --write **/*.{ts,js,css,html,tsx}\",\n    \"compile\": \"tsc\",\n    \"workflow\": \"yarn compile && yarn prettier && yarn lint && yarn test --watchAll=false\",\n    \"cypress\": \"npx cypress open\"\n  },\n  \"dependencies\": {\n    \"@chakra-ui/react\": \"^2.7.1\",\n    \"@chakra-ui/theme-tools\": \"^2.0.18\",\n    \"@emotion/react\": \"^11.11.1\",\n    \"@emotion/styled\": \"^11.11.0\",\n    \"@tanstack/react-query\": \"^4.29.15\",\n    \"@testing-library/jest-dom\": \"^5.16.5\",\n    \"axios\": \"^1.4.0\",\n    \"chakra-ui-autocomplete\": \"^1.4.5\",\n    \"dayjs\": \"^1.11.8\",\n    \"formik\": \"^2.4.2\",\n    \"framer-motion\": \"^10.12.17\",\n    \"msw\": \"^1.2.2\",\n    \"react\": \"^18.2.0\",\n    \"react-color\": \"^2.19.3\",\n    \"react-contexify\": \"^5.0.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"react-easy-crop\": \"^4.7.5\",\n    \"react-icons\": \"4.10.1\",\n    \"react-infinite-scroll-component\": \"^6.1.0\",\n    \"react-router-dom\": \"^6.14.0\",\n    \"react-scripts\": \"5.0.1\",\n    \"react-textarea-autosize\": \"^8.5.0\",\n    \"reconnecting-websocket\": \"^4.4.0\",\n    \"yup\": \"^1.2.0\",\n    \"zustand\": \"^4.3.8\"\n  },\n  \"devDependencies\": {\n    \"@testing-library/dom\": \"^9.3.1\",\n    \"@testing-library/react\": \"^14.0.0\",\n    \"@testing-library/react-hooks\": \"^8.0.1\",\n    \"@testing-library/user-event\": \"^14.4.3\",\n    \"@types/node\": \"^20.3.1\",\n    \"@types/react\": \"^18.2.14\",\n    \"@types/react-color\": \"^3.0.6\",\n    \"@types/react-dom\": \"^18.2.6\",\n    \"@types/react-router-dom\": \"^5.3.3\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.60.0\",\n    \"@typescript-eslint/parser\": \"^5.60.0\",\n    \"cypress\": \"12.15.0\",\n    \"eslint\": \"^8.43.0\",\n    \"eslint-config-airbnb\": \"^19.0.4\",\n    \"eslint-config-airbnb-typescript\": \"^17.0.0\",\n    \"eslint-config-prettier\": \"^8.8.0\",\n    \"eslint-import-resolver-typescript\": \"^3.5.5\",\n    \"eslint-plugin-import\": \"^2.27.5\",\n    \"eslint-plugin-jsx-a11y\": \"^6.7.1\",\n    \"eslint-plugin-prettier\": \"^4.2.1\",\n    \"eslint-plugin-react\": \"^7.32.2\",\n    \"eslint-plugin-react-hooks\": \"^4.6.0\",\n    \"prettier\": \"^2.8.8\",\n    \"react-test-renderer\": \"^18.2.0\",\n    \"typescript\": \"^5.1.3\"\n  },\n  \"eslintConfig\": {\n    \"extends\": \"react-app\"\n  },\n  \"browserslist\": {\n    \"production\": [\n      \">0.2%\",\n      \"not dead\",\n      \"not op_mini all\"\n    ],\n    \"development\": [\n      \"last 1 chrome version\",\n      \"last 1 firefox version\",\n      \"last 1 safari version\"\n    ]\n  },\n  \"jest\": {\n    \"transform\": {\n      \"^.+\\\\.[t|j]sx?$\": \"babel-jest\"\n    },\n    \"transformIgnorePatterns\": [\n      \"node_modules/(?!axios)/\"\n    ]\n  }\n}\n"
  },
  {
    "path": "web/public/_redirects",
    "content": "/*    /index.html   200"
  },
  {
    "path": "web/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <meta\n      name=\"description\"\n      content=\"A Discord clone using React and Go. Check https://github.com/sentrionic/Valkyrie for more info\"\n    />\n    <link rel=\"apple-touch-icon\" href=\"%PUBLIC_URL%/logo192.png\" />\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <link rel=\"preconnect\" href=\"https://fonts.gstatic.com\" />\n    <link href=\"https://fonts.googleapis.com/css2?family=Open+Sans&display=swap\" rel=\"stylesheet\" />\n    <title>Valkyrie</title>\n  </head>\n  <body>\n    <noscript>You need to enable JavaScript to run this app.</noscript>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "web/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    },\n    {\n      \"src\": \"logo192.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"192x192\"\n    },\n    {\n      \"src\": \"logo512.png\",\n      \"type\": \"image/png\",\n      \"sizes\": \"512x512\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "web/public/robots.txt",
    "content": "# https://www.robotstxt.org/robotstxt.html\nUser-agent: *\nDisallow:\n"
  },
  {
    "path": "web/src/App.tsx",
    "content": "import * as React from 'react';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { AppRoutes } from './routes/Routes';\nimport { GlobalState } from './components/sections/GlobalState';\n\nconst client = new QueryClient({\n  defaultOptions: {\n    queries: {\n      refetchOnWindowFocus: false,\n      staleTime: Infinity,\n      cacheTime: 0,\n    },\n  },\n});\n\nexport const App: React.FC = () => (\n  <QueryClientProvider client={client}>\n    <GlobalState>\n      <AppRoutes />\n    </GlobalState>\n  </QueryClientProvider>\n);\n"
  },
  {
    "path": "web/src/components/common/GuildPills.tsx",
    "content": "import { Box } from '@chakra-ui/react';\nimport React from 'react';\n\nexport const NotificationIndicator: React.FC = () => (\n  <Box w=\"8px\" h=\"8px\" bg=\"white\" position=\"absolute\" borderRadius=\"0 4px 4px 0\" ml=\"-4px\" mt=\"20px\" left={0} />\n);\n\nexport const ChannelNotificationIndicator: React.FC = () => (\n  <Box w=\"8px\" h=\"8px\" bg=\"white\" position=\"absolute\" borderRadius=\"0 4px 4px 0\" ml=\"-4px\" mt=\"8px\" left=\"-10px\" />\n);\n\nexport const ActiveGuildPill: React.FC = () => (\n  <Box w=\"8px\" h=\"40px\" bg=\"white\" position=\"absolute\" borderRadius=\"0 4px 4px 0\" ml=\"-4px\" left={0} mt=\"4px\" />\n);\n\nexport const HoverGuildPill: React.FC = () => (\n  <Box w=\"8px\" h=\"24px\" bg=\"white\" position=\"absolute\" borderRadius=\"0 4px 4px 0\" ml=\"-4px\" left={0} mt=\"12px\" />\n);\n"
  },
  {
    "path": "web/src/components/common/InputField.tsx",
    "content": "/* eslint-disable react/jsx-props-no-spreading */\nimport React, { InputHTMLAttributes } from 'react';\nimport { useField } from 'formik';\nimport { FormControl, FormErrorMessage, FormLabel, Input, Text } from '@chakra-ui/react';\n\ntype InputFieldProps = InputHTMLAttributes<HTMLInputElement> & {\n  label: string;\n  name: string;\n};\n\nexport const InputField: React.FC<InputFieldProps> = ({ label, ...props }) => {\n  const [field, { error, touched }] = useField(props);\n  return (\n    <FormControl mt={4} isInvalid={error != null && touched}>\n      <FormLabel htmlFor={field.name}>\n        <Text fontSize=\"12px\" textTransform=\"uppercase\">\n          {label}\n        </Text>\n      </FormLabel>\n      {/* @ts-ignore */}\n      <Input\n        bg=\"brandGray.dark\"\n        borderColor=\"black\"\n        borderRadius=\"3px\"\n        focusBorderColor=\"highlight.standard\"\n        {...field}\n        {...props}\n      />\n      <FormErrorMessage>{error}</FormErrorMessage>\n    </FormControl>\n  );\n};\n"
  },
  {
    "path": "web/src/components/common/Logo.tsx",
    "content": "import { Box, Image, Text } from '@chakra-ui/react';\nimport React from 'react';\n\nexport const Logo: React.FC = () => (\n  <Box w=\"80px\" color={['white', 'white', 'primary.500', 'primary.500']}>\n    <Text fontSize=\"lg\" fontWeight=\"bold\">\n      <Image src={`${process.env.PUBLIC_URL}/logo.png`} />\n    </Text>\n  </Box>\n);\n"
  },
  {
    "path": "web/src/components/common/NotificationIcon.tsx",
    "content": "import { Flex, Text } from '@chakra-ui/react';\nimport React from 'react';\n\ninterface NotificationIconProps {\n  count: number;\n}\n\nexport const NotificationIcon: React.FC<NotificationIconProps> = ({ count }) => (\n  <Flex\n    borderRadius=\"50%\"\n    bg=\"menuRed\"\n    position=\"absolute\"\n    bottom={0}\n    right={0}\n    transform=\"translate(25%, 25%)\"\n    border=\"0.3em solid\"\n    borderColor=\"brandBorder\"\n    w=\"1.4em\"\n    h=\"1.4em\"\n    justify=\"center\"\n    align=\"center\"\n  >\n    <Text fontSize=\"12px\" fontWeight=\"bold\" color=\"white\">\n      {count}\n    </Text>\n  </Flex>\n);\n\nexport const PingIcon: React.FC<NotificationIconProps> = ({ count }) => (\n  <Flex borderRadius=\"50%\" bg=\"menuRed\" w=\"1.2em\" h=\"1.2em\" justify=\"center\" align=\"center\" ml={2}>\n    <Text fontSize=\"11px\" fontWeight=\"bold\" color=\"white\">\n      {count}\n    </Text>\n  </Flex>\n);\n"
  },
  {
    "path": "web/src/components/items/ChannelListItem.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Flex, Icon, ListItem, Text, useDisclosure } from '@chakra-ui/react';\nimport { FaHashtag, FaUserLock } from 'react-icons/fa';\nimport { MdSettings } from 'react-icons/md';\nimport { Link, useLocation } from 'react-router-dom';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { userStore } from '../../lib/stores/userStore';\nimport { ChannelSettingsModal } from '../modals/ChannelSettingsModal';\nimport { useGetCurrentGuild } from '../../lib/utils/hooks/useGetCurrentGuild';\nimport { ChannelNotificationIndicator } from '../common/GuildPills';\nimport { cKey } from '../../lib/utils/querykeys';\nimport { Channel } from '../../lib/models/channel';\n\ninterface ChannelListItemProps {\n  channel: Channel;\n  guildId: string;\n}\n\nexport const ChannelListItem: React.FC<ChannelListItemProps> = ({ channel, guildId }) => {\n  const currentPath = `/channels/${guildId}/${channel.id}`;\n  const location = useLocation();\n  const isActive = location.pathname === currentPath;\n  const [showSettings, setShowSettings] = useState(false);\n\n  const current = userStore((state) => state.current);\n  const guild = useGetCurrentGuild(guildId);\n\n  const { isOpen, onOpen, onClose } = useDisclosure();\n\n  const cache = useQueryClient();\n\n  useEffect(() => {\n    if (channel.hasNotification && isActive) {\n      cache.setQueryData<Channel[]>([cKey, guildId], (d) => {\n        if (!d) return [];\n        return d.map((c) => (c.id === channel.id ? { ...c, hasNotification: false } : c));\n      });\n    }\n  });\n\n  return (\n    <Link to={currentPath}>\n      <ListItem\n        p=\"5px\"\n        m=\"0 10px\"\n        color={isActive || channel.hasNotification ? '#fff' : 'brandGray.accent'}\n        _hover={{\n          bg: 'brandGray.light',\n          borderRadius: '5px',\n          cursor: 'pointer',\n          color: '#fff',\n        }}\n        bg={isActive ? 'brandGray.active' : undefined}\n        mb=\"2px\"\n        position=\"relative\"\n        onMouseLeave={() => setShowSettings(false)}\n        onMouseEnter={() => setShowSettings(true)}\n      >\n        {channel.hasNotification && <ChannelNotificationIndicator />}\n        <Flex align=\"center\" justify=\"space-between\">\n          <Flex align=\"center\" textOverflow=\"ellipsis\" maxW=\"80%\">\n            <Icon as={channel.isPublic ? FaHashtag : FaUserLock} color=\"brandGray.accent\" />\n            <Text ml=\"2\" noOfLines={1}>\n              {channel.name}\n            </Text>\n          </Flex>\n          {current?.id === guild?.ownerId && (showSettings || isOpen) && (\n            <>\n              <Icon\n                aria-label=\"edit channel\"\n                as={MdSettings}\n                color=\"brandGray.accent\"\n                fontSize=\"12px\"\n                _hover={{ color: '#fff' }}\n                onClick={(e) => {\n                  e.preventDefault();\n                  onOpen();\n                }}\n              />\n              {isOpen && (\n                <ChannelSettingsModal guildId={guildId} channelId={channel.id} isOpen={isOpen} onClose={onClose} />\n              )}\n            </>\n          )}\n        </Flex>\n      </ListItem>\n    </Link>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/DMListItem.tsx",
    "content": "import React, { useState } from 'react';\nimport { Avatar, AvatarBadge, Flex, Icon, ListItem, Text } from '@chakra-ui/react';\nimport { Link, useNavigate, useLocation } from 'react-router-dom';\nimport { IoMdClose } from 'react-icons/io';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { closeDirectMessage } from '../../lib/api/handler/dm';\nimport { dmKey } from '../../lib/utils/querykeys';\nimport { DMChannel } from '../../lib/models/dm';\n\ninterface DMListItemProps {\n  dm: DMChannel;\n}\n\nexport const DMListItem: React.FC<DMListItemProps> = ({ dm }) => {\n  const currentPath = `/channels/me/${dm.id}`;\n  const location = useLocation();\n  const isActive = location.pathname === currentPath;\n  const [showCloseButton, setShowButton] = useState(false);\n  const navigate = useNavigate();\n  const cache = useQueryClient();\n\n  const closeDM = async (): Promise<void> => {\n    try {\n      await closeDirectMessage(dm.id);\n      cache.setQueryData<DMChannel[]>([dmKey], (d) => d?.filter((c) => c.id !== dm.id) ?? []);\n      if (isActive) {\n        navigate('/channels/me', { replace: true });\n      }\n    } catch (err) {}\n  };\n\n  return (\n    <Link to={`/channels/me/${dm.id}`}>\n      <ListItem\n        p=\"2\"\n        mx=\"2\"\n        color={isActive ? '#fff' : 'brandGray.accent'}\n        _hover={{\n          bg: 'brandGray.light',\n          borderRadius: '5px',\n          cursor: 'pointer',\n          color: '#fff',\n        }}\n        bg={isActive ? 'brandGray.active' : undefined}\n        onMouseLeave={() => setShowButton(false)}\n        onMouseEnter={() => setShowButton(true)}\n      >\n        <Flex align=\"center\" justify=\"space-between\">\n          <Flex align=\"center\" textOverflow=\"ellipsis\" maxW=\"80%\">\n            <Avatar size=\"sm\" src={dm.user.image}>\n              <AvatarBadge boxSize=\"1.25em\" bg={dm.user.isOnline ? 'green.500' : 'gray.500'} />\n            </Avatar>\n            <Text ml=\"2\" noOfLines={1}>\n              {dm.user.username}\n            </Text>\n          </Flex>\n          {showCloseButton && (\n            <Icon\n              aria-label=\"close dm\"\n              as={IoMdClose}\n              onClick={async (e) => {\n                e.preventDefault();\n                await closeDM();\n              }}\n            />\n          )}\n        </Flex>\n      </ListItem>\n    </Link>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/FriendsListItem.tsx",
    "content": "import { Avatar, AvatarBadge, Flex, IconButton, ListItem, Text, useDisclosure } from '@chakra-ui/react';\nimport React from 'react';\nimport { FaEllipsisV } from 'react-icons/fa';\nimport { useNavigate } from 'react-router-dom';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { getOrCreateDirectMessage } from '../../lib/api/handler/dm';\nimport { RemoveFriendModal } from '../modals/RemoveFriendModal';\nimport { dmKey } from '../../lib/utils/querykeys';\nimport { Friend } from '../../lib/models/friend';\nimport { DMChannel } from '../../lib/models/dm';\n\ninterface FriendsListItemProp {\n  friend: Friend;\n}\n\nexport const FriendsListItem: React.FC<FriendsListItemProp> = ({ friend }) => {\n  const navigate = useNavigate();\n  const { isOpen, onOpen, onClose } = useDisclosure();\n  const cache = useQueryClient();\n\n  const getDMChannel = async (): Promise<void> => {\n    try {\n      const { data } = await getOrCreateDirectMessage(friend.id);\n      if (data) {\n        cache.setQueryData<DMChannel[]>([dmKey], (d) => {\n          const queryData = d ?? [];\n          const index = queryData.findIndex((dm) => dm.id === data.id);\n          if (index === -1) return [data, ...queryData];\n          return queryData;\n        });\n        navigate(`/channels/me/${data.id}`);\n      }\n    } catch (err) {}\n  };\n\n  return (\n    <ListItem\n      p=\"3\"\n      mx=\"3\"\n      _hover={{\n        bg: 'brandGray.dark',\n        borderRadius: '5px',\n      }}\n    >\n      <Flex align=\"center\" justify=\"space-between\">\n        <Flex align=\"center\" w=\"full\" onClick={getDMChannel} _hover={{ cursor: 'pointer' }}>\n          <Avatar size=\"sm\" src={friend.image}>\n            <AvatarBadge boxSize=\"1.25em\" bg={friend.isOnline ? 'green.500' : 'gray.500'} />\n          </Avatar>\n          <Text ml=\"2\">{friend.username}</Text>\n        </Flex>\n        <IconButton\n          icon={<FaEllipsisV />}\n          borderRadius=\"50%\"\n          aria-label=\"remove friend\"\n          onClick={(e) => {\n            e.preventDefault();\n            onOpen();\n          }}\n        />\n      </Flex>\n      {isOpen && <RemoveFriendModal member={friend} isOpen onClose={onClose} />}\n    </ListItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/GuildListItem.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Avatar, Flex } from '@chakra-ui/react';\nimport { Link, useLocation } from 'react-router-dom';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { StyledTooltip } from '../sections/StyledTooltip';\nimport { ActiveGuildPill, HoverGuildPill, NotificationIndicator } from '../common/GuildPills';\nimport { gKey } from '../../lib/utils/querykeys';\nimport { Guild } from '../../lib/models/guild';\n\ninterface GuildListItemProps {\n  guild: Guild;\n}\n\nexport const GuildListItem: React.FC<GuildListItemProps> = ({ guild }) => {\n  const location = useLocation();\n  const isActive = location.pathname.includes(guild.id);\n  const [isHover, setHover] = useState(false);\n  const cache = useQueryClient();\n\n  useEffect(() => {\n    if (guild.hasNotification && isActive) {\n      cache.setQueryData<Guild[]>([gKey], (data) => {\n        if (!data) return [];\n        return data.map((g) => (g.id === guild.id ? { ...g, hasNotification: false } : g));\n      });\n    }\n  });\n\n  return (\n    <Flex mb=\"2\" justify=\"center\" position=\"relative\">\n      {isActive && <ActiveGuildPill />}\n      {isHover && <HoverGuildPill />}\n      {guild.hasNotification && <NotificationIndicator />}\n      <StyledTooltip label={guild.name} position=\"right\">\n        <Link to={`/channels/${guild.id}/${guild.default_channel_id}`}>\n          {guild.icon ? (\n            <Avatar\n              src={guild.icon}\n              borderRadius={isActive || isHover ? '35%' : '50%'}\n              name={guild.name}\n              color=\"#fff\"\n              bg=\"brandGray.light\"\n              onMouseLeave={() => setHover(false)}\n              onMouseEnter={() => setHover(true)}\n            />\n          ) : (\n            <Flex\n              justify=\"center\"\n              align=\"center\"\n              bg={isActive ? 'highlight.standard' : 'brandGray.light'}\n              borderRadius={isActive ? '35%' : '50%'}\n              h=\"48px\"\n              w=\"48px\"\n              color={isActive ? 'white' : undefined}\n              fontSize=\"20px\"\n              _hover={{\n                borderRadius: '35%',\n                bg: 'highlight.standard',\n                color: 'white',\n              }}\n              onMouseLeave={() => setHover(false)}\n              onMouseEnter={() => setHover(true)}\n            >\n              {guild.name[0]}\n            </Flex>\n          )}\n        </Link>\n      </StyledTooltip>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/MemberListItem.tsx",
    "content": "import React from 'react';\nimport { Avatar, AvatarBadge, Flex, ListItem, Text } from '@chakra-ui/react';\nimport { useContextMenu } from 'react-contexify';\nimport { useParams } from 'react-router-dom';\nimport { useGetCurrentGuild } from '../../lib/utils/hooks/useGetCurrentGuild';\nimport { userStore } from '../../lib/stores/userStore';\nimport { MemberContextMenu } from '../menus/MemberContextMenu';\nimport { RouterProps } from '../../lib/models/routerProps';\nimport { Member } from '../../lib/models/member';\n\ninterface MemberListItemProps {\n  member: Member;\n}\n\nexport const MemberListItem: React.FC<MemberListItemProps> = ({ member }) => {\n  const current = userStore((state) => state.current);\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n  const guild = useGetCurrentGuild(guildId);\n  const isOwner = guild !== undefined && guild.ownerId === current?.id;\n\n  const { show } = useContextMenu({\n    id: member.id,\n  });\n\n  return (\n    <>\n      <ListItem\n        p=\"2\"\n        mx=\"10px\"\n        color=\"brandGray.accent\"\n        _hover={{\n          bg: 'brandGray.light',\n          borderRadius: '5px',\n          cursor: 'pointer',\n          color: '#fff',\n        }}\n        onContextMenu={show}\n      >\n        <Flex align=\"center\">\n          <Avatar size=\"sm\" src={member.image}>\n            <AvatarBadge boxSize=\"1.25em\" bg={member.isOnline ? 'green.500' : 'gray.500'} />\n          </Avatar>\n          <Text ml=\"2\" textOverflow=\"ellipsis\" maxW=\"80%\" noOfLines={1} color={member.color ?? undefined}>\n            {member.nickname ?? member.username}\n          </Text>\n        </Flex>\n      </ListItem>\n      {member.id !== current?.id && <MemberContextMenu member={member} isOwner={isOwner} id={member.id} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/NotificationListItem.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Avatar, Flex } from '@chakra-ui/react';\nimport { Link, useLocation } from 'react-router-dom';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { StyledTooltip } from '../sections/StyledTooltip';\nimport { ActiveGuildPill, HoverGuildPill, NotificationIndicator } from '../common/GuildPills';\nimport { NotificationIcon } from '../common/NotificationIcon';\nimport { dmKey, nKey } from '../../lib/utils/querykeys';\nimport { DMChannel, DMNotification } from '../../lib/models/dm';\n\ninterface NotificationListItemProps {\n  notification: DMNotification;\n}\n\nexport const NotificationListItem: React.FC<NotificationListItemProps> = ({ notification }) => {\n  const location = useLocation();\n  const isActive = location.pathname.includes(notification.id);\n  const [isHover, setHover] = useState(false);\n  const cache = useQueryClient();\n\n  useEffect(() => {\n    if (isActive) {\n      cache.setQueryData<DMNotification[]>([nKey], (d) => d?.filter((c) => c.id !== notification.id) ?? []);\n    }\n  });\n\n  const handleClick = (): void => {\n    if (window.location.pathname.includes('/channels/me')) {\n      const newChannel: DMChannel = {\n        id: notification.id,\n        user: notification.user,\n      };\n\n      cache.setQueryData<DMChannel[]>([dmKey], (d) => {\n        const data = d ?? [];\n        const index = d!.findIndex((dm) => dm.id === notification.id);\n        if (index === -1) return [newChannel, ...data];\n        return data;\n      });\n    }\n  };\n\n  return (\n    <Flex mb=\"2\" justify=\"center\" position=\"relative\">\n      {isActive && <ActiveGuildPill />}\n      {isHover && <HoverGuildPill />}\n      <NotificationIndicator />\n      <StyledTooltip label={notification.user.username} position=\"right\">\n        <Link to={`/channels/me/${notification.id}`}>\n          <Avatar\n            src={notification.user.image}\n            borderRadius={isActive || isHover ? '35%' : '50%'}\n            name={notification.user.username}\n            color=\"#fff\"\n            bg=\"brandGray.light\"\n            onMouseLeave={() => setHover(false)}\n            onMouseEnter={() => setHover(true)}\n            onClick={() => handleClick()}\n          >\n            <NotificationIcon count={notification.count} />\n          </Avatar>\n        </Link>\n      </StyledTooltip>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/RequestListItem.tsx",
    "content": "import { Avatar, Box, Flex, IconButton, ListItem, Text } from '@chakra-ui/react';\nimport React from 'react';\nimport { BiCheck } from 'react-icons/bi';\nimport { AiOutlineClose } from 'react-icons/ai';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { StyledTooltip } from '../sections/StyledTooltip';\nimport { acceptFriendRequest, declineFriendRequest } from '../../lib/api/handler/account';\nimport { fKey, rKey } from '../../lib/utils/querykeys';\nimport { FriendRequest, RequestType } from '../../lib/models/friend';\n\ninterface RequestListItemProps {\n  request: FriendRequest;\n}\n\nexport const RequestListItem: React.FC<RequestListItemProps> = ({ request }) => {\n  const cache = useQueryClient();\n\n  const acceptRequest = async (): Promise<void> => {\n    try {\n      const { data } = await acceptFriendRequest(request.id);\n      if (data) {\n        cache.setQueryData<FriendRequest[]>([rKey], (d) => d?.filter((r) => r.id !== request.id) ?? []);\n        await cache.invalidateQueries([fKey]);\n      }\n    } catch (err) {}\n  };\n\n  const declineRequest = async (): Promise<void> => {\n    try {\n      const { data } = await declineFriendRequest(request.id);\n      if (data) {\n        cache.setQueryData<FriendRequest[]>([rKey], (d) => d?.filter((r) => r.id !== request.id) ?? []);\n      }\n    } catch (err) {}\n  };\n\n  return (\n    <ListItem\n      p=\"3\"\n      mx=\"3\"\n      _hover={{\n        bg: 'brandGray.dark',\n        borderRadius: '5px',\n      }}\n    >\n      <Flex align=\"center\" justify=\"space-between\">\n        <Flex align=\"center\">\n          <Avatar size=\"sm\" src={request.image} />\n          <Box ml=\"2\">\n            <Text>{request.username}</Text>\n            <Text fontSize=\"12px\">\n              {request.type === RequestType.INCOMING ? 'Incoming Friend Request' : 'Outgoing Friend Request'}\n            </Text>\n          </Box>\n        </Flex>\n        <Flex align=\"center\">\n          {request.type === 1 && (\n            <StyledTooltip label=\"Accept\" position=\"top\">\n              <IconButton\n                icon={<BiCheck />}\n                borderRadius=\"50%\"\n                aria-label=\"accept request\"\n                fontSize=\"28px\"\n                onClick={acceptRequest}\n                mr=\"2\"\n              />\n            </StyledTooltip>\n          )}\n          <StyledTooltip label=\"Decline\" position=\"top\">\n            <IconButton\n              icon={<AiOutlineClose />}\n              borderRadius=\"50%\"\n              aria-label=\"decline request\"\n              fontSize=\"20px\"\n              onClick={declineRequest}\n            />\n          </StyledTooltip>\n        </Flex>\n      </Flex>\n    </ListItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/VoiceChannelItem.tsx",
    "content": "import { Avatar, Flex, Icon, Text } from '@chakra-ui/react';\nimport React, { useCallback } from 'react';\nimport { MdHeadsetOff, MdMicOff } from 'react-icons/md';\n\ninterface VoiceUserVisualProps {\n  username: string;\n  image: string;\n  isMuted: boolean;\n  isDeafened: boolean;\n}\n\nconst VoiceUserVisual: React.FC<VoiceUserVisualProps> = ({ username, image, isMuted, isDeafened }) => (\n  <Flex\n    py=\"1\"\n    px=\"2\"\n    align=\"center\"\n    w=\"80%\"\n    mr={2}\n    ml={8}\n    mb=\"1\"\n    justify=\"space-between\"\n    _hover={{\n      bg: 'brandGray.light',\n      borderRadius: '5px',\n      cursor: 'pointer',\n      color: '#fff',\n    }}\n  >\n    <Flex>\n      <Avatar size=\"xs\" src={image} />\n      <Text ml=\"2\" fontSize={14}>\n        {username}\n      </Text>\n    </Flex>\n    {isDeafened && <Icon as={MdHeadsetOff} color=\"brandGray.accent\" />}\n    {isMuted && <Icon as={MdMicOff} color=\"brandGray.accent\" />}\n  </Flex>\n);\n\ninterface VoiceUserProps extends VoiceUserVisualProps {\n  stream?: MediaStream | null;\n  muted?: boolean;\n  controls?: boolean;\n}\n\nexport const VoiceChannelItem: React.FC<VoiceUserProps> = ({\n  username,\n  image,\n  stream,\n  muted = false,\n  controls = false,\n  isMuted,\n  isDeafened,\n}) => {\n  const refAudio = useCallback(\n    (node: HTMLAudioElement) => {\n      if (node && stream) {\n        const audio = node;\n        audio.srcObject = stream;\n      }\n    },\n    [stream]\n  );\n\n  if (!stream) return <VoiceUserVisual image={image} username={username} isMuted={isMuted} isDeafened={isDeafened} />;\n\n  return (\n    <>\n      <VoiceUserVisual image={image} username={username} isMuted={isMuted} isDeafened={isDeafened} />\n      {/* eslint-disable-next-line jsx-a11y/media-has-caption */}\n      <audio autoPlay ref={refAudio} muted={muted} controls={controls} />\n    </>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/css/ContextMenu.css",
    "content": ".react-contexify {\n  position: fixed;\n  opacity: 0;\n  -webkit-user-select: none;\n  -moz-user-select: none;\n  -ms-user-select: none;\n  user-select: none;\n  background-color: #18191c;\n  box-sizing: border-box;\n  box-shadow: 0px 10px 30px -5px rgba(0, 0, 0, 0.3);\n  border-radius: 6px;\n  padding: 6px;\n  min-width: 200px;\n  z-index: 100;\n  font-size: 14px;\n}\n\n.react-contexify__submenu--is-open > .react-contexify__submenu {\n  pointer-events: initial;\n  opacity: 1;\n}\n\n.react-contexify .react-contexify__submenu {\n  position: absolute;\n  /* negate padding */\n  top: -6px;\n  pointer-events: none;\n  transition: opacity 0.275s;\n}\n.react-contexify__submenu-arrow {\n  margin-left: auto;\n  font-size: 12px;\n}\n.react-contexify__separator {\n  width: 100%;\n  height: 1px;\n  cursor: default;\n  margin: 4px 0;\n  background-color: rgba(0, 0, 0, 0.2);\n}\n.react-contexify__will-leave--disabled {\n  pointer-events: none;\n}\n.react-contexify__item {\n  cursor: pointer;\n  position: relative;\n}\n.react-contexify__item:focus {\n  outline: 0;\n}\n\n.menu-item:hover {\n  color: white;\n  background-color: #7289da;\n  border-radius: 2px;\n}\n\n.delete-item {\n  color: #f04747 !important;\n}\n\n.delete-item:hover {\n  color: white;\n  background-color: #f04747;\n  border-radius: 2px;\n}\n\n.react-contexify__item:not(.react-contexify__item--disabled):hover > .react-contexify__submenu {\n  pointer-events: initial;\n  opacity: 1;\n}\n.react-contexify__item--disabled {\n  cursor: default;\n  opacity: 0.5;\n}\n.react-contexify__item__content {\n  padding: 6px 12px;\n  display: -ms-flexbox;\n  display: flex;\n  -ms-flex-align: center;\n  align-items: center;\n  white-space: nowrap;\n  color: #333;\n  position: relative;\n}\n\n.react-contexify__theme--dark {\n  background-color: rgba(40, 40, 40, 0.98);\n}\n.react-contexify__theme--dark .react-contexify__submenu {\n  background-color: rgba(40, 40, 40, 0.98);\n}\n.react-contexify__theme--dark .react-contexify__separator {\n  background-color: #eee;\n}\n\n.react-contexify__item__content {\n  color: #ffffff;\n}\n\n.react-contexify__theme--light .react-contexify__separator {\n  background-color: #eee;\n}\n.react-contexify__theme--light .react-contexify__submenu--is-open,\n.react-contexify__theme--light .react-contexify__submenu--is-open > .react-contexify__item__content {\n  color: #4393e6;\n  background-color: #e0eefd;\n}\n.react-contexify__theme--light\n  .react-contexify__item:not(.react-contexify__item--disabled):hover\n  > .react-contexify__item__content,\n.react-contexify__theme--light\n  .react-contexify__item:not(.react-contexify__item--disabled):focus\n  > .react-contexify__item__content {\n  color: #4393e6;\n  background-color: #e0eefd;\n}\n.react-contexify__theme--light .react-contexify__item__content {\n  color: #666;\n}\n\n@keyframes react-contexify__scaleIn {\n  from {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n  to {\n    opacity: 1;\n  }\n}\n@keyframes react-contexify__scaleOut {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n    transform: scale3d(0.3, 0.3, 0.3);\n  }\n}\n.react-contexify__will-enter--scale {\n  transform-origin: top left;\n  animation: react-contexify__scaleIn 0.3s;\n}\n\n.react-contexify__will-leave--scale {\n  transform-origin: top left;\n  animation: react-contexify__scaleOut 0.3s;\n}\n\n@keyframes react-contexify__fadeIn {\n  from {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n  to {\n    opacity: 1;\n    transform: translateY(0);\n  }\n}\n@keyframes react-contexify__fadeOut {\n  from {\n    opacity: 1;\n    transform: translateY(0);\n  }\n  to {\n    opacity: 0;\n    transform: translateY(10px);\n  }\n}\n.react-contexify__will-enter--fade {\n  animation: react-contexify__fadeIn 0.3s ease;\n}\n\n.react-contexify__will-leave--fade {\n  animation: react-contexify__fadeOut 0.3s ease;\n}\n\n@keyframes react-contexify__flipInX {\n  from {\n    transform: perspective(800px) rotate3d(1, 0, 0, 45deg);\n  }\n  to {\n    transform: perspective(800px);\n  }\n}\n@keyframes react-contexify__flipOutX {\n  from {\n    transform: perspective(800px);\n  }\n  to {\n    transform: perspective(800px) rotate3d(1, 0, 0, 45deg);\n    opacity: 0;\n  }\n}\n.react-contexify__will-enter--flip {\n  -webkit-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n  transform-origin: top center;\n  animation: react-contexify__flipInX 0.3s;\n}\n\n.react-contexify__will-leave--flip {\n  transform-origin: top center;\n  animation: react-contexify__flipOutX 0.3s;\n  -webkit-backface-visibility: visible !important;\n  backface-visibility: visible !important;\n}\n\n@keyframes swing-in-top-fwd {\n  0% {\n    transform: rotateX(-100deg);\n    transform-origin: top;\n    opacity: 0;\n  }\n  100% {\n    transform: rotateX(0deg);\n    transform-origin: top;\n    opacity: 1;\n  }\n}\n@keyframes react-contexify__slideIn {\n  from {\n    opacity: 0;\n    transform: scale3d(1, 0.3, 1);\n  }\n  to {\n    opacity: 1;\n  }\n}\n@keyframes react-contexify__slideOut {\n  from {\n    opacity: 1;\n  }\n  to {\n    opacity: 0;\n    transform: scale3d(1, 0.3, 1);\n  }\n}\n.react-contexify__will-enter--slide {\n  transform-origin: top center;\n  animation: react-contexify__slideIn 0.3s;\n}\n\n.react-contexify__will-leave--slide {\n  transform-origin: top center;\n  animation: react-contexify__slideOut 0.3s;\n}\n"
  },
  {
    "path": "web/src/components/items/message/Message.tsx",
    "content": "import React, { useState } from 'react';\nimport { Avatar, Box, Flex, Icon, Text, useDisclosure } from '@chakra-ui/react';\nimport { Item, Menu, theme, useContextMenu } from 'react-contexify';\nimport { useParams } from 'react-router-dom';\nimport { MdEdit } from 'react-icons/md';\nimport { FaEllipsisH, FaRegTrashAlt } from 'react-icons/fa';\nimport { FiLink } from 'react-icons/fi';\nimport { MessageContent } from './MessageContent';\nimport { userStore } from '../../../lib/stores/userStore';\nimport { getShortenedTime, getTime } from '../../../lib/utils/dateUtils';\nimport { DeleteMessageModal } from '../../modals/DeleteMessageModal';\nimport { EditMessageModal } from '../../modals/EditMessageModal';\nimport { useGetCurrentGuild } from '../../../lib/utils/hooks/useGetCurrentGuild';\nimport { MemberContextMenu } from '../../menus/MemberContextMenu';\nimport { UserPopover } from '../../sections/UserPopover';\nimport { RouterProps } from '../../../lib/models/routerProps';\nimport { Message as MessageResponse } from '../../../lib/models/message';\nimport '../css/ContextMenu.css';\n\ninterface MessageProps {\n  message: MessageResponse;\n  isCompact?: boolean;\n}\n\nexport const Message: React.FC<MessageProps> = ({ message, isCompact = false }) => {\n  const [showSettings, setShowSettings] = useState(false);\n  const current = userStore((state) => state.current);\n  const isAuthor = current?.id === message.user.id;\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n  const guild = useGetCurrentGuild(guildId);\n  const isOwner = guild !== undefined && guild.ownerId === current?.id;\n  const showMenu = isAuthor || isOwner || message.attachment?.url;\n\n  const { isOpen: isDeleteOpen, onOpen: onDeleteOpen, onClose: onDeleteClose } = useDisclosure();\n  const { isOpen: isEditOpen, onOpen: onEditOpen, onClose: onEditClose } = useDisclosure();\n  const id = `${message.user.id}-${Math.random().toString(36).substr(2, 5)}`;\n\n  const { show } = useContextMenu({\n    id: message.id,\n  });\n\n  const { show: profileShow } = useContextMenu({ id });\n\n  const openInNewTab = (url: string): void => {\n    const newWindow = window.open(url, '_blank', 'noopener,noreferrer');\n    if (newWindow) newWindow.opener = null;\n  };\n\n  return (\n    <>\n      <Flex\n        alignItems=\"center\"\n        mr=\"1\"\n        mt={isCompact ? '0' : '3'}\n        _hover={{ bg: 'brandGray.hover' }}\n        justify=\"space-between\"\n        onMouseLeave={() => setShowSettings(false)}\n        onMouseEnter={() => setShowSettings(true)}\n      >\n        <Flex w=\"full\">\n          {isCompact ? (\n            <>\n              <Box ml=\"3\" minW=\"44px\" textAlign=\"center\">\n                <Text fontSize=\"10px\" color=\"brandGray.accent\" mt=\"1\" hidden={!showSettings}>\n                  {getShortenedTime(message.createdAt)}\n                </Text>\n              </Box>\n\n              <Box ml=\"3\" w=\"full\" onContextMenu={show}>\n                <MessageContent message={message} />\n              </Box>\n              {showSettings && showMenu ? (\n                <Box onClick={show} mr=\"2\" _hover={{ cursor: 'pointer' }} h=\"5px\">\n                  <FaEllipsisH />\n                </Box>\n              ) : (\n                <Box mr=\"6\" />\n              )}\n            </>\n          ) : (\n            <>\n              <UserPopover member={message.user}>\n                <Avatar\n                  h=\"40px\"\n                  w=\"40px\"\n                  ml=\"4\"\n                  mt=\"1\"\n                  src={message.user.image}\n                  _hover={{\n                    cursor: 'pointer',\n                  }}\n                  onContextMenu={(e) => {\n                    if (!isAuthor) profileShow(e);\n                  }}\n                />\n              </UserPopover>\n              <Box ml=\"3\" w=\"full\" onContextMenu={show}>\n                <Flex alignItems=\"center\" justify=\"space-between\">\n                  <Flex alignItems=\"center\">\n                    <Text color={message.user.color ?? undefined}>\n                      {message.user.nickname ?? message.user.username}\n                    </Text>\n                    <Text fontSize=\"12px\" color=\"brandGray.accent\" ml=\"2\">\n                      {getTime(message.createdAt)}\n                    </Text>\n                  </Flex>\n                  {showSettings && showMenu && (\n                    <Box onClick={show} mr=\"2\" _hover={{ cursor: 'pointer' }}>\n                      <FaEllipsisH />\n                    </Box>\n                  )}\n                </Flex>\n                <MessageContent message={message} />\n              </Box>\n            </>\n          )}\n        </Flex>\n      </Flex>\n      {showMenu && (\n        <>\n          <Menu id={message.id} theme={theme.dark}>\n            {message.attachment?.filetype ? (\n              <Item\n                className=\"menu-item\"\n                onClick={() => {\n                  if (message.attachment?.url) openInNewTab(message.attachment.url);\n                }}\n              >\n                <Flex align=\"center\" justify=\"space-between\" w=\"full\">\n                  <Text>Open Link</Text>\n                  <Icon as={FiLink} />\n                </Flex>\n              </Item>\n            ) : (\n              isAuthor && (\n                <Item className=\"menu-item\" onClick={onEditOpen}>\n                  <Flex align=\"center\" justify=\"space-between\" w=\"full\">\n                    <Text>Edit Message</Text>\n                    <Icon as={MdEdit} />\n                  </Flex>\n                </Item>\n              )\n            )}\n            {(isAuthor || isOwner) && (\n              <Item onClick={onDeleteOpen} className=\"delete-item\">\n                <Flex align=\"center\" justify=\"space-between\" w=\"full\">\n                  <Text>Delete Message</Text>\n                  <Icon as={FaRegTrashAlt} />\n                </Flex>\n              </Item>\n            )}\n          </Menu>\n          {isDeleteOpen && <DeleteMessageModal message={message} isOpen={isDeleteOpen} onClose={onDeleteClose} />}\n          {isEditOpen && <EditMessageModal message={message} isOpen={isEditOpen} onClose={onEditClose} />}\n        </>\n      )}\n      {!isAuthor && <MemberContextMenu member={message.user} isOwner={isOwner} id={id} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "web/src/components/items/message/MessageContent.tsx",
    "content": "import React from 'react';\nimport { Box, Flex, Image, Text } from '@chakra-ui/react';\nimport { Message } from '../../../lib/models/message';\n\ninterface MessageProps {\n  message: Message;\n}\n\nexport const MessageContent: React.FC<MessageProps> = ({ message: { attachment, text, createdAt, updatedAt } }) => {\n  if (attachment) {\n    const { filetype, url } = attachment;\n    if (filetype.startsWith('image/')) {\n      return (\n        <Box boxSize=\"sm\" my=\"2\" h=\"full\">\n          <Image fit=\"contain\" src={url} alt=\"\" borderRadius=\"md\" />\n        </Box>\n      );\n    }\n    if (filetype.startsWith('audio/')) {\n      return (\n        <Box my=\"2\">\n          {/* eslint-disable-next-line jsx-a11y/media-has-caption */}\n          <audio controls>\n            <source src={url} type={filetype} />\n          </audio>\n        </Box>\n      );\n    }\n  }\n  return (\n    <Flex alignItems=\"center\">\n      <Text>{text}</Text>\n      {createdAt !== updatedAt && (\n        <Text fontSize=\"10px\" ml=\"1\" color=\"labelGray\">\n          (edited)\n        </Text>\n      )}\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/AccountBar.tsx",
    "content": "import { Avatar, Flex, IconButton, Text, Tooltip, useClipboard } from '@chakra-ui/react';\nimport React from 'react';\nimport { MdHeadset, MdHeadsetOff, MdMic, MdMicOff } from 'react-icons/md';\nimport { RiSettings5Fill } from 'react-icons/ri';\nimport { Link } from 'react-router-dom';\nimport { userStore } from '../../lib/stores/userStore';\nimport { voiceStore } from '../../lib/stores/voiceStore';\n\nexport const AccountBar: React.FC = () => {\n  const user = userStore((state) => state.current);\n  const [isMuted, isDeafened, setIsMuted, setIsDeafened] = voiceStore((state) => [\n    state.isMuted,\n    state.isDeafened,\n    state.setIsMuted,\n    state.setIsDeafened,\n  ]);\n  const { hasCopied, onCopy } = useClipboard(user?.id || '');\n\n  const handleMute = (): void => {\n    setIsMuted(!isMuted);\n    // socket.send(\n    //   JSON.stringify({\n    //     action: 'toggle-mute',\n    //     room: guildId,\n    //     message: { id: user?.id, value: !isMuted },\n    //   })\n    // );\n  };\n\n  const handleDeafen = (): void => {\n    setIsDeafened(!isDeafened);\n    // socket.send(\n    //   JSON.stringify({\n    //     action: 'toggle-deafen',\n    //     room: guildId,\n    //     message: { id: user?.id, value: !isDeafened },\n    //   })\n    // );\n  };\n\n  return (\n    <Flex p=\"10px\" pos=\"absolute\" bottom=\"0\" w=\"240px\" bg=\"accountBar\" align=\"center\" justify=\"space-between\">\n      <Tooltip\n        hasArrow\n        label={hasCopied ? 'Copied!' : 'Click to copy ID'}\n        placement=\"top\"\n        bg={hasCopied ? 'brandGreen' : 'brandGray.darkest'}\n        color=\"white\"\n        closeOnClick={false}\n      >\n        <Flex\n          align=\"center\"\n          maxW=\"50%\"\n          w=\"full\"\n          mr={2}\n          _hover={{ cursor: 'pointer' }}\n          onClick={onCopy}\n          textOverflow=\"ellipsis\"\n        >\n          <Avatar size=\"sm\" src={user?.image} />\n          <Text noOfLines={1} ml=\"2\" fontSize=\"14px\">\n            {user?.username}\n          </Text>\n        </Flex>\n      </Tooltip>\n      <Flex>\n        <Tooltip\n          hasArrow\n          label={isDeafened || isMuted ? 'Unmute' : 'Mute'}\n          placement=\"top\"\n          bg=\"brandGray.darkest\"\n          color=\"white\"\n        >\n          <IconButton\n            icon={isMuted ? <MdMicOff /> : <MdMic />}\n            aria-label=\"toggle mute mic\"\n            size=\"sm\"\n            fontSize=\"22px\"\n            variant=\"ghost\"\n            onClick={() => handleMute()}\n          />\n        </Tooltip>\n        <Tooltip\n          hasArrow\n          label={isDeafened ? 'Undeafen' : 'Deafen'}\n          placement=\"top\"\n          bg=\"brandGray.darkest\"\n          color=\"white\"\n        >\n          <IconButton\n            icon={isDeafened ? <MdHeadsetOff /> : <MdHeadset />}\n            aria-label=\"toggle deafen audio\"\n            size=\"sm\"\n            fontSize=\"20px\"\n            variant=\"ghost\"\n            onClick={() => handleDeafen()}\n          />\n        </Tooltip>\n        <Link to=\"/account\">\n          <Tooltip hasArrow label=\"User Settings\" placement=\"top\" bg=\"brandGray.darkest\" color=\"white\">\n            <IconButton icon={<RiSettings5Fill />} aria-label=\"settings\" size=\"sm\" fontSize=\"20px\" variant=\"ghost\" />\n          </Tooltip>\n        </Link>\n      </Flex>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/AppLayout.tsx",
    "content": "import { Grid } from '@chakra-ui/react';\nimport React from 'react';\n\ninterface AppLayoutProps {\n  showLastColumn?: boolean | null;\n  children: React.ReactNode;\n}\n\nexport const AppLayout: React.FC<AppLayoutProps> = ({ showLastColumn = false, children }) => (\n  // Col Chat: GuildList ChannelList Chat [MemberList]\n  // Col Home: GuildList DMList Chat/FriendsList\n  <Grid\n    height=\"100vh\"\n    templateColumns={`75px 240px 1fr ${showLastColumn ? '240px' : ''} `}\n    templateRows=\"auto 1fr auto\"\n    bg=\"brandGray.light\"\n  >\n    {children}\n  </Grid>\n);\n"
  },
  {
    "path": "web/src/components/layouts/LandingLayout.tsx",
    "content": "import React from 'react';\nimport { Flex } from '@chakra-ui/react';\nimport { NavBar } from '../sections/NavBar';\nimport { Footer } from '../sections/Footer';\n\ninterface IProps {\n  children: React.ReactNode;\n}\n\nexport const LandingLayout: React.FC<IProps> = ({ children }) => (\n  <Flex direction=\"column\" align=\"center\" maxW={{ xl: '1200px' }} m=\"0 auto\">\n    <NavBar />\n    {children}\n    <Footer />\n  </Flex>\n);\n"
  },
  {
    "path": "web/src/components/layouts/VoiceBar.tsx",
    "content": "import { Box, Flex, Icon, IconButton, Text, Tooltip } from '@chakra-ui/react';\nimport React from 'react';\nimport { AiFillSignal } from 'react-icons/ai';\nimport { HiPhoneMissedCall } from 'react-icons/hi';\nimport { voiceStore } from '../../lib/stores/voiceStore';\nimport { useGetCurrentGuild } from '../../lib/utils/hooks/useGetCurrentGuild';\n\nexport const VoiceBar: React.FC = () => {\n  const [voiceChatID, inVC, leaveVoice] = voiceStore((state) => [state.voiceChatID, state.inVC, state.leaveVoice]);\n  const guild = useGetCurrentGuild(voiceChatID);\n\n  if (!inVC) return <Box />;\n\n  return (\n    <Flex p=\"10px\" pos=\"absolute\" bottom=\"54px\" w=\"240px\" bg=\"accountBar\" align=\"center\" justify=\"space-between\">\n      <Box>\n        <Flex>\n          <Icon as={AiFillSignal} color=\"brandGreen\" mr=\"1\" />\n          <Text color=\"brandGreen\" fontSize={13} fontWeight=\"semibold\">\n            Voice Connected\n          </Text>\n        </Flex>\n        <Text fontSize={12} color=\"brandGray.accent\">\n          General / {guild?.name}\n        </Text>\n      </Box>\n      <Tooltip hasArrow label=\"Disconnect\" placement=\"top\" bg=\"brandGray.darkest\" color=\"white\">\n        <IconButton\n          icon={<HiPhoneMissedCall />}\n          aria-label=\"disconnect from call\"\n          size=\"sm\"\n          fontSize=\"20px\"\n          variant=\"ghost\"\n          onClick={() => leaveVoice()}\n        />\n      </Tooltip>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/ChannelHeader.tsx",
    "content": "import React from 'react';\nimport { Flex, GridItem, Icon, Text } from '@chakra-ui/react';\nimport { FaHashtag } from 'react-icons/fa';\nimport { BsPeopleFill } from 'react-icons/bs';\nimport { useParams } from 'react-router-dom';\nimport { settingsStore } from '../../../lib/stores/settingsStore';\nimport { useGetCurrentChannel } from '../../../lib/utils/hooks/useGetCurrentChannel';\nimport { RouterProps } from '../../../lib/models/routerProps';\n\nexport const ChannelHeader: React.FC = () => {\n  const toggleMemberList = settingsStore((state) => state.toggleShowMembers);\n  const { guildId, channelId } = useParams<keyof RouterProps>() as RouterProps;\n  const channel = useGetCurrentChannel(channelId, guildId);\n\n  return (\n    <GridItem gridColumn={3} gridRow=\"1\" bg=\"brandGray.light\" padding=\"10px\" zIndex=\"2\" boxShadow=\"md\">\n      <Flex align=\"center\" justify=\"space-between\">\n        <Flex align=\"center\">\n          <FaHashtag />\n          <Text ml=\"2\" fontWeight=\"semibold\">\n            {channel?.name}\n          </Text>\n        </Flex>\n        <Icon\n          as={BsPeopleFill}\n          fontSize=\"20px\"\n          mr=\"2\"\n          _hover={{ cursor: 'pointer' }}\n          onClick={toggleMemberList}\n          aria-label=\"toggle member list\"\n        />\n      </Flex>\n    </GridItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/Channels.tsx",
    "content": "import React from 'react';\nimport { Box, GridItem, UnorderedList, useDisclosure } from '@chakra-ui/react';\nimport { useParams } from 'react-router-dom';\nimport { useQuery } from '@tanstack/react-query';\nimport { AccountBar } from '../AccountBar';\nimport { CreateChannelModal } from '../../modals/CreateChannelModal';\nimport { GuildMenu } from '../../menus/GuildMenu';\nimport { InviteModal } from '../../modals/InviteModal';\nimport { ChannelListItem } from '../../items/ChannelListItem';\nimport { cKey } from '../../../lib/utils/querykeys';\nimport { channelScrollbarCss } from './css/ChannelScrollerCSS';\nimport { useChannelSocket } from '../../../lib/api/ws/useChannelSocket';\nimport { getChannels } from '../../../lib/api/handler/channel';\nimport { RouterProps } from '../../../lib/models/routerProps';\nimport { VoiceChat } from './VoiceChat';\nimport { VoiceBar } from '../VoiceBar';\n\nexport const Channels: React.FC = () => {\n  const { isOpen: inviteIsOpen, onOpen: inviteOpen, onClose: inviteClose } = useDisclosure();\n  const { isOpen: channelIsOpen, onOpen: channelOpen, onClose: channelClose } = useDisclosure();\n\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n\n  const { data } = useQuery([cKey, guildId], () => getChannels(guildId).then((response) => response.data));\n\n  useChannelSocket(guildId);\n\n  return (\n    <>\n      <GuildMenu channelOpen={channelOpen} inviteOpen={inviteOpen} />\n      <GridItem\n        gridColumn={2}\n        gridRow=\"2/4\"\n        bg=\"brandGray.dark\"\n        overflowY=\"hidden\"\n        _hover={{ overflowY: 'auto' }}\n        css={channelScrollbarCss}\n      >\n        {inviteIsOpen && <InviteModal isOpen={inviteIsOpen} onClose={inviteClose} />}\n        {channelIsOpen && <CreateChannelModal guildId={guildId} onClose={channelClose} isOpen={channelIsOpen} />}\n        <UnorderedList listStyleType=\"none\" ml=\"0\" mt=\"4\">\n          {data?.map((c) => (\n            <ChannelListItem channel={c} guildId={guildId} key={`${c.id}`} />\n          ))}\n          <VoiceChat />\n          <Box h=\"16\" />\n        </UnorderedList>\n        <VoiceBar />\n        <AccountBar />\n      </GridItem>\n    </>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/GuildList.tsx",
    "content": "import React from 'react';\nimport { Box, Divider, Flex, GridItem, UnorderedList, useDisclosure } from '@chakra-ui/react';\nimport { useQuery } from '@tanstack/react-query';\nimport { AddGuildModal } from '../../modals/AddGuildModal';\nimport { GuildListItem } from '../../items/GuildListItem';\nimport { AddGuildIcon } from '../../sections/AddGuildIcon';\nimport { HomeIcon } from '../../sections/HomeIcon';\nimport { getUserGuilds } from '../../../lib/api/handler/guilds';\nimport { gKey, nKey } from '../../../lib/utils/querykeys';\nimport { guildScrollbarCss } from './css/GuildScrollerCSS';\nimport { useGuildSocket } from '../../../lib/api/ws/useGuildSocket';\nimport { NotificationListItem } from '../../items/NotificationListItem';\nimport { DMNotification } from '../../../lib/models/dm';\n\nexport const GuildList: React.FC = () => {\n  const { isOpen, onOpen, onClose } = useDisclosure();\n\n  const { data } = useQuery([gKey], () => getUserGuilds().then((response) => response.data), {\n    cacheTime: Infinity,\n  });\n\n  const { data: dmData } = useQuery<DMNotification[]>([nKey], () => [], {\n    cacheTime: Infinity,\n  });\n\n  useGuildSocket();\n\n  return (\n    <GridItem gridColumn={1} gridRow=\"1 / 4\" bg=\"brandGray.darker\" overflowY=\"auto\" css={guildScrollbarCss} zIndex={2}>\n      <HomeIcon />\n      <UnorderedList listStyleType=\"none\" ml=\"0\" id=\"guild-list\">\n        {dmData?.map((dm) => (\n          <NotificationListItem notification={dm} key={dm.id} />\n        ))}\n      </UnorderedList>\n      <Flex direction=\"column\" my=\"2\" align=\"center\">\n        <Divider w=\"40px\" />\n      </Flex>\n      <UnorderedList listStyleType=\"none\" ml=\"0\">\n        {data?.map((g) => (\n          <GuildListItem guild={g} key={g.id} />\n        ))}\n      </UnorderedList>\n      <AddGuildIcon onOpen={onOpen} />\n      {isOpen && <AddGuildModal isOpen={isOpen} onClose={onClose} />}\n      <Box h=\"20px\" />\n    </GridItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/MemberList.tsx",
    "content": "import React from 'react';\nimport { GridItem, UnorderedList } from '@chakra-ui/react';\nimport { useParams } from 'react-router-dom';\nimport { useQuery } from '@tanstack/react-query';\nimport { MemberListItem } from '../../items/MemberListItem';\nimport { getGuildMembers } from '../../../lib/api/handler/guilds';\nimport { mKey } from '../../../lib/utils/querykeys';\nimport { memberScrollbarCss } from './css/MemberScrollerCSS';\nimport { useMemberSocket } from '../../../lib/api/ws/useMemberSocket';\nimport { OnlineLabel } from '../../sections/OnlineLabel';\nimport { RouterProps } from '../../../lib/models/routerProps';\nimport { Member } from '../../../lib/models/member';\n\nexport const MemberList: React.FC = () => {\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n  const key = [mKey, guildId];\n\n  const { data } = useQuery(key, () => getGuildMembers(guildId).then((response) => response.data));\n\n  const online: Member[] = [];\n  const offline: Member[] = [];\n\n  if (data) {\n    data.forEach((m) => {\n      if (m.isOnline) {\n        online.push(m);\n      } else {\n        offline.push(m);\n      }\n    });\n  }\n\n  useMemberSocket(guildId);\n\n  return (\n    <GridItem\n      gridColumn={4}\n      gridRow=\"1 / 4\"\n      bg=\"memberList\"\n      overflowY=\"hidden\"\n      _hover={{ overflowY: 'auto' }}\n      css={memberScrollbarCss}\n    >\n      <UnorderedList listStyleType=\"none\" ml=\"0\" id=\"member-list\">\n        <OnlineLabel label={`online—${online.length}`} />\n        {online.map((m) => (\n          <MemberListItem key={`${m.id}`} member={m} />\n        ))}\n        <OnlineLabel label={`offline—${offline.length}`} />\n        {offline.map((m) => (\n          <MemberListItem key={`${m.id}`} member={m} />\n        ))}\n      </UnorderedList>\n    </GridItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/VoiceChat.tsx",
    "content": "import { Box, Flex, Icon, ListItem, Text } from '@chakra-ui/react';\nimport React from 'react';\nimport { FaVolumeUp } from 'react-icons/fa';\nimport { useQuery } from '@tanstack/react-query';\nimport { useParams } from 'react-router-dom';\nimport { getVCMembers } from '../../../lib/api/handler/guilds';\nimport { useVoiceSocket } from '../../../lib/api/ws/useVoiceSocket';\nimport { RouterProps } from '../../../lib/models/routerProps';\nimport { userStore } from '../../../lib/stores/userStore';\nimport { voiceStore } from '../../../lib/stores/voiceStore';\nimport { useSetupVoiceChat } from '../../../lib/utils/hooks/useVoiceChat';\nimport { vcKey } from '../../../lib/utils/querykeys';\nimport { VoiceChannelItem } from '../../items/VoiceChannelItem';\n\nexport const VoiceChat: React.FC<{}> = () => {\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n  const current = userStore((state) => state.current);\n  const key = [vcKey, guildId];\n\n  const [voiceChatID, setVoiceID] = voiceStore((state) => [state.voiceChatID, state.setVoiceID]);\n  const [inVC, setIsInVC] = voiceStore((state) => [state.inVC, state.setInVC]);\n  const voiceClients = voiceStore((state) => state.voiceClients);\n\n  const [localStream, setLocalStream] = voiceStore((state) => [state.localStream, state.setLocalStream]);\n\n  const [isMuted, isDeafened] = voiceStore((state) => [state.isMuted, state.isDeafened]);\n  const leaveVoice = voiceStore((state) => state.leaveVoice);\n\n  const { data } = useQuery(key, () => getVCMembers(guildId).then((response) => response.data));\n\n  useVoiceSocket();\n  useSetupVoiceChat(guildId);\n\n  const joinVoice = async (): Promise<void> => {\n    if (guildId === voiceChatID) return;\n    if (voiceChatID !== '') {\n      leaveVoice();\n    }\n    await openMic();\n    setIsInVC(true);\n    setVoiceID(guildId);\n  };\n\n  const openMic = async (): Promise<void> => {\n    try {\n      // Get audio device and set better audio settings\n      const result = await navigator.mediaDevices.getUserMedia({\n        audio: {\n          autoGainControl: false,\n          channelCount: 2,\n          echoCancellation: false,\n          noiseSuppression: false,\n          sampleRate: 48000,\n          sampleSize: 16,\n        },\n      });\n      setLocalStream(result);\n    } catch (err) {}\n  };\n\n  return (\n    <Box>\n      <ListItem\n        p=\"5px\"\n        m=\"0 10px\"\n        color=\"brandGray.accent\"\n        _hover={{\n          bg: 'brandGray.light',\n          borderRadius: '5px',\n          cursor: 'pointer',\n          color: '#fff',\n        }}\n        mb=\"2px\"\n        position=\"relative\"\n        onClick={() => joinVoice()}\n      >\n        <Flex align=\"center\" justify=\"space-between\">\n          <Flex align=\"center\">\n            <Icon as={FaVolumeUp} color=\"brandGray.accent\" />\n            <Text ml=\"2\">General</Text>\n          </Flex>\n        </Flex>\n      </ListItem>\n      <Box>\n        {/* Current user */}\n        {inVC && localStream && guildId === voiceChatID && (\n          <VoiceChannelItem\n            image={current!.image}\n            username={current!.username}\n            stream={localStream}\n            controls={false}\n            muted\n            isMuted={isMuted}\n            isDeafened={isDeafened}\n          />\n        )}\n        {/* User is in VC, render all other clients */}\n        {guildId === voiceChatID\n          ? voiceClients.map(\n              (e) =>\n                e.id !== current?.id && (\n                  <VoiceChannelItem\n                    image={e.image}\n                    username={e.username}\n                    stream={e.stream}\n                    key={e.id}\n                    muted={isDeafened}\n                    isMuted={e.isMuted}\n                    isDeafened={e.isDeafened}\n                  />\n                )\n            )\n          : // User is not in VC, render all clients without the stream\n            data?.map((e) => (\n              <VoiceChannelItem\n                image={e.image}\n                username={e.username}\n                key={e.id}\n                isMuted={e.isMuted}\n                isDeafened={e.isDeafened}\n              />\n            ))}\n      </Box>\n    </Box>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/chat/ChatGrid.tsx",
    "content": "import React from 'react';\nimport { GridItem } from '@chakra-ui/react';\nimport { scrollbarCss } from '../../../../lib/utils/theme';\n\ninterface IProps {\n  children: React.ReactNode;\n}\n\nexport const ChatGrid: React.FC<IProps> = ({ children }) => (\n  <GridItem\n    id=\"chatGrid\"\n    gridColumn={3}\n    gridRow=\"2\"\n    bg=\"brandGray.light\"\n    mr=\"5px\"\n    display=\"flex\"\n    flexDirection=\"column-reverse\"\n    overflowY=\"auto\"\n    css={scrollbarCss}\n  >\n    {children}\n  </GridItem>\n);\n"
  },
  {
    "path": "web/src/components/layouts/guild/chat/ChatScreen.tsx",
    "content": "import React, { useState } from 'react';\nimport { Box, Flex, Spinner } from '@chakra-ui/react';\nimport InfiniteScroll from 'react-infinite-scroll-component';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport { useParams } from 'react-router-dom';\nimport { Message } from '../../../items/message/Message';\nimport { StartMessages } from '../../../sections/StartMessages';\nimport { getMessages } from '../../../../lib/api/handler/messages';\nimport { checkNewDay, getTimeDifference } from '../../../../lib/utils/dateUtils';\nimport { guildScrollbarCss } from '../css/GuildScrollerCSS';\nimport { useMessageSocket } from '../../../../lib/api/ws/useMessageSocket';\nimport { DateDivider } from '../../../sections/DateDivider';\nimport { ChatGrid } from './ChatGrid';\nimport { RouterProps } from '../../../../lib/models/routerProps';\nimport { Message as MessageResponse } from '../../../../lib/models/message';\nimport { msgKey } from '../../../../lib/utils/querykeys';\n\nexport const ChatScreen: React.FC = () => {\n  const { channelId } = useParams<keyof RouterProps>() as RouterProps;\n  const [hasMore, setHasMore] = useState(true);\n\n  const { data, isLoading, fetchNextPage } = useInfiniteQuery<MessageResponse[]>(\n    [msgKey, channelId],\n    async ({ pageParam = null }) => {\n      const { data: messageData } = await getMessages(channelId, pageParam);\n      if (messageData.length !== 35) setHasMore(false);\n      return messageData;\n    },\n    {\n      staleTime: 0,\n      cacheTime: 0,\n      getNextPageParam: (lastPage) => (hasMore && lastPage.length ? lastPage[lastPage.length - 1].createdAt : ''),\n    }\n  );\n\n  useMessageSocket(channelId);\n\n  if (isLoading) {\n    return (\n      <ChatGrid>\n        <Flex align=\"center\" justify=\"center\" h=\"full\">\n          <Spinner size=\"xl\" thickness=\"4px\" />\n        </Flex>\n      </ChatGrid>\n    );\n  }\n\n  const checkIfWithinTime = (message1: MessageResponse, message2: MessageResponse): boolean => {\n    if (message1.user.id !== message2.user.id) return false;\n    if (message1.createdAt === message2.createdAt) return false;\n    return getTimeDifference(message1.createdAt, message2.createdAt) <= 5;\n  };\n\n  const messages = data ? data!.pages.map((p) => p.map((mr) => mr)).flat() : [];\n\n  return (\n    <ChatGrid>\n      <Box h=\"10px\" mt={4} />\n      <Box\n        as={InfiniteScroll as any}\n        css={guildScrollbarCss}\n        dataLength={messages.length}\n        next={() => fetchNextPage()}\n        style={{\n          display: 'flex',\n          flexDirection: 'column-reverse',\n        }}\n        inverse\n        hasMore={hasMore}\n        loader={\n          messages.length > 0 && (\n            <Flex align=\"center\" justify=\"center\" h=\"50px\">\n              <Spinner />\n            </Flex>\n          )\n        }\n        scrollableTarget=\"chatGrid\"\n      >\n        {messages.map((m, i) => (\n          <React.Fragment key={m.id}>\n            <Message message={m} isCompact={checkIfWithinTime(m, messages[Math.min(i + 1, messages.length - 1)])} />\n            {checkNewDay(m.createdAt, messages[Math.min(i + 1, messages.length - 1)].createdAt) && (\n              <DateDivider date={m.createdAt} />\n            )}\n          </React.Fragment>\n        ))}\n      </Box>\n      {!hasMore && <StartMessages />}\n    </ChatGrid>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/chat/FileUploadButton.tsx",
    "content": "import {\n  Icon,\n  InputLeftElement,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalHeader,\n  ModalOverlay,\n  Progress,\n  Text,\n  useDisclosure,\n} from '@chakra-ui/react';\nimport React, { useRef, useState } from 'react';\nimport { MdAddCircle } from 'react-icons/md';\nimport { useParams } from 'react-router-dom';\nimport { sendMessage } from '../../../../lib/api/handler/messages';\nimport { FileSchema } from '../../../../lib/utils/validation/message.schema';\nimport { StyledTooltip } from '../../../sections/StyledTooltip';\nimport { RouterProps } from '../../../../lib/models/routerProps';\n\nexport const FileUploadButton: React.FC = () => {\n  const { channelId } = useParams<keyof RouterProps>() as RouterProps;\n  const { isOpen, onOpen, onClose } = useDisclosure();\n\n  const inputFile: any = useRef(null);\n  const [isSubmitting, setSubmitting] = useState(false);\n  const [progress, setProgress] = useState(0);\n  const [errors, setErrors] = useState({});\n  const disable = process.env.NODE_ENV === 'production';\n\n  const closeModal = (): void => {\n    setErrors({});\n    setProgress(0);\n    onClose();\n  };\n\n  const handleSubmit = async (file: File): Promise<void> => {\n    if (!file) return;\n    setSubmitting(true);\n\n    try {\n      await FileSchema.validate({ file });\n    } catch (err: any) {\n      setErrors(err.errors);\n      onOpen();\n      return;\n    }\n\n    const data = new FormData();\n    data.append('file', file);\n    await sendMessage(channelId, data, (event: any) => {\n      const loaded = Math.round((100 * event.loaded) / event.total);\n      setProgress(loaded);\n      if (loaded >= 100) setProgress(0);\n    });\n  };\n\n  return (\n    <StyledTooltip disabled={!disable} label=\"File Upload is disabled on the test site\" position=\"top\">\n      <InputLeftElement\n        color=\"iconColor\"\n        _hover={{\n          cursor: 'pointer',\n          color: '#fcfcfc',\n        }}\n        onClick={() => inputFile.current.click()}\n      >\n        <Icon as={MdAddCircle} boxSize=\"20px\" />\n        <input\n          type=\"file\"\n          ref={inputFile}\n          hidden\n          disabled={isSubmitting || disable}\n          onChange={async (e) => {\n            if (!e.currentTarget.files) return;\n            handleSubmit(e.currentTarget.files[0]).then(() => {\n              setSubmitting(false);\n              e.target.value = '';\n            });\n          }}\n        />\n        {errors && (\n          <Modal size=\"sm\" isOpen={isOpen} onClose={closeModal} isCentered>\n            <ModalOverlay />\n            <ModalContent bg=\"brandGray.light\" textAlign=\"center\">\n              <ModalHeader pb=\"0\">Error Uploading File</ModalHeader>\n              <ModalCloseButton _focus={{ outline: 'none' }} />\n              <ModalBody>\n                <Text mb=\"2\">\n                  Reason: <>{errors}</>\n                </Text>\n                <Text>Max file size is 5.00 MB</Text>\n                <Text>Only Images and mp3 allowed</Text>\n              </ModalBody>\n            </ModalContent>\n          </Modal>\n        )}\n        {progress > 0 && (\n          <Modal size=\"sm\" isOpen={progress > 0} closeOnOverlayClick={false} onClose={closeModal} isCentered>\n            <ModalContent bg=\"brandGray.darker\" textAlign=\"center\">\n              <ModalHeader pb=\"0\">Upload Progress</ModalHeader>\n              <ModalBody>\n                <Progress hasStripe isAnimated value={progress} />\n              </ModalBody>\n            </ModalContent>\n          </Modal>\n        )}\n      </InputLeftElement>\n    </StyledTooltip>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/chat/MessageInput.tsx",
    "content": "import React, { useRef, useState } from 'react';\nimport { Flex, GridItem, InputGroup, Text, Textarea } from '@chakra-ui/react';\nimport ResizeTextarea from 'react-textarea-autosize';\nimport { useParams } from 'react-router-dom';\nimport { useQuery } from '@tanstack/react-query';\nimport { FileUploadButton } from './FileUploadButton';\nimport { sendMessage } from '../../../../lib/api/handler/messages';\nimport { getSameSocket } from '../../../../lib/api/getSocket';\nimport { userStore } from '../../../../lib/stores/userStore';\nimport { channelStore } from '../../../../lib/stores/channelStore';\nimport { cKey, dmKey } from '../../../../lib/utils/querykeys';\nimport { RouterProps } from '../../../../lib/models/routerProps';\nimport '../css/MessageInput.css';\n\nexport const MessageInput: React.FC = () => {\n  const [text, setText] = useState('');\n  const [isSubmitting, setSubmitting] = useState(false);\n  const [currentlyTyping, setCurrentlyTyping] = useState(false);\n  const inputRef: any = useRef();\n\n  const { guildId, channelId } = useParams<keyof RouterProps>() as RouterProps;\n  const qKey = guildId === undefined ? [dmKey] : [cKey, guildId];\n  const { data } = useQuery<any[]>(qKey);\n  const channel = data?.find((c) => c.id === channelId);\n\n  const socket = getSameSocket();\n  const current = userStore((state) => state.current);\n  const isTyping = channelStore((state) => state.typing);\n\n  const handleSubmit = async (): Promise<void> => {\n    if (!text || !text.trim()) {\n      return;\n    }\n\n    socket.send(\n      JSON.stringify({\n        action: 'stopTyping',\n        room: channelId,\n        message: current?.username,\n      })\n    );\n\n    try {\n      setSubmitting(true);\n      setCurrentlyTyping(false);\n      const formData = new FormData();\n      formData.append('text', text.trim());\n      await sendMessage(channelId, formData);\n    } catch (err) {}\n  };\n\n  const getTypingString = (members: string[]): string => {\n    switch (members.length) {\n      case 1:\n        return members[0];\n      case 2:\n        return `${members[0]} and ${members[1]}`;\n      case 3:\n        return `${members[0]}, ${members[1]} and ${members[2]}`;\n      default:\n        return 'Several people';\n    }\n  };\n\n  const getPlaceholder = (): string => {\n    if (!channel) return '';\n\n    if (channel?.user) {\n      return `Message @${channel?.user.username}`;\n    }\n    return `Message #${channel?.name}`;\n  };\n\n  return (\n    <GridItem gridColumn={3} gridRow={3} px=\"20px\" pb={isTyping.length > 0 ? '0' : '26px'} bg=\"brandGray.light\">\n      <InputGroup size=\"md\" bg=\"messageInput\" alignItems=\"center\" borderRadius=\"8px\">\n        <Textarea\n          as={ResizeTextarea}\n          minH=\"40px\"\n          transition=\"height none\"\n          overflow=\"hidden\"\n          w=\"100%\"\n          resize=\"none\"\n          minRows={1}\n          pl=\"3rem\"\n          name=\"text\"\n          placeholder={getPlaceholder()}\n          border=\"0\"\n          _focus={{ border: '0' }}\n          ref={inputRef}\n          isDisabled={isSubmitting}\n          value={text}\n          onChange={(e) => {\n            const { value } = e.target;\n            if (value.trim().length === 1 && !currentlyTyping) {\n              socket.send(\n                JSON.stringify({\n                  action: 'startTyping',\n                  room: channelId,\n                  message: current?.username,\n                })\n              );\n              setCurrentlyTyping(true);\n            } else if (value.length === 0) {\n              socket.send(\n                JSON.stringify({\n                  action: 'stopTyping',\n                  room: channelId,\n                  message: current?.username,\n                })\n              );\n              setCurrentlyTyping(false);\n            }\n            if (value.length <= 2000) setText(value);\n          }}\n          onKeyDown={(e) => {\n            if (e.key === 'Enter') {\n              handleSubmit().then(() => {\n                setText('');\n                setSubmitting(false);\n                inputRef?.current?.focus();\n              });\n            }\n          }}\n        />\n        <FileUploadButton />\n      </InputGroup>\n      {isTyping.length > 0 && (\n        <Flex align=\"center\" fontSize=\"12px\" my={1}>\n          <div className=\"typing-indicator\">\n            <span />\n            <span />\n            <span />\n          </div>\n          <Text ml=\"1\" fontWeight=\"semibold\">\n            {getTypingString(isTyping)}\n          </Text>\n          <Text ml=\"1\">{isTyping.length === 1 ? 'is' : 'are'} typing... </Text>\n        </Flex>\n      )}\n    </GridItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/css/ChannelScrollerCSS.ts",
    "content": "export const channelScrollbarCss = {\n  '&::-webkit-scrollbar': {\n    width: '4px',\n  },\n  '&::-webkit-scrollbar-track': {\n    width: '4px',\n  },\n  '&::-webkit-scrollbar-thumb': {\n    background: 'brandGray.darker',\n    borderRadius: '18px',\n  },\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/css/GuildScrollerCSS.ts",
    "content": "export const guildScrollbarCss = {\n  '&::-webkit-scrollbar': {\n    width: '0',\n  },\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/css/MemberScrollerCSS.ts",
    "content": "export const memberScrollbarCss = {\n  '&::-webkit-scrollbar': {\n    width: '4px',\n  },\n  '&::-webkit-scrollbar-track': {\n    width: '4px',\n  },\n  '&::-webkit-scrollbar-thumb': {\n    background: 'brandGray.darker',\n    borderRadius: '18px',\n  },\n};\n"
  },
  {
    "path": "web/src/components/layouts/guild/css/MessageInput.css",
    "content": ".typing-indicator {\n  border-radius: 50px;\n  display: table;\n  position: relative;\n  -webkit-animation: 2s bulge infinite ease-out;\n  animation: 2s bulge infinite ease-out;\n}\n\n.typing-indicator span {\n  height: 7px;\n  width: 7px;\n  float: left;\n  margin: 0 1px;\n  background-color: #fff;\n  display: block;\n  border-radius: 50%;\n  opacity: 0.4;\n}\n.typing-indicator span:nth-of-type(1) {\n  -webkit-animation: 1s blink infinite 0.3333s;\n  animation: 1s blink infinite 0.3333s;\n}\n.typing-indicator span:nth-of-type(2) {\n  -webkit-animation: 1s blink infinite 0.6666s;\n  animation: 1s blink infinite 0.6666s;\n}\n.typing-indicator span:nth-of-type(3) {\n  -webkit-animation: 1s blink infinite 0.9999s;\n  animation: 1s blink infinite 0.9999s;\n}\n\n@-webkit-keyframes blink {\n  50% {\n    opacity: 1;\n  }\n}\n\n@keyframes blink {\n  50% {\n    opacity: 1;\n  }\n}\n@-webkit-keyframes bulge {\n  50% {\n    -webkit-transform: scale(1.05);\n    transform: scale(1.05);\n  }\n}\n@keyframes bulge {\n  50% {\n    -webkit-transform: scale(1.05);\n    transform: scale(1.05);\n  }\n}\n"
  },
  {
    "path": "web/src/components/layouts/home/DMHeader.tsx",
    "content": "import React from 'react';\nimport { Box, Flex, GridItem, Icon, Text } from '@chakra-ui/react';\nimport { FaAt } from 'react-icons/fa';\nimport { useParams } from 'react-router-dom';\nimport { useGetCurrentDM } from '../../../lib/utils/hooks/useGetCurrentDM';\nimport { RouterProps } from '../../../lib/models/routerProps';\n\nexport const DMHeader: React.FC = () => {\n  const { channelId } = useParams<keyof RouterProps>() as RouterProps;\n  const channel = useGetCurrentDM(channelId);\n\n  return (\n    <GridItem gridColumn={3} gridRow=\"1\" bg=\"brandGray.light\" padding=\"10px\" zIndex=\"2\" boxShadow=\"md\">\n      <Flex align=\"center\" ml={2}>\n        <Icon as={FaAt} fontSize=\"20px\" color=\"brandGray.accent\" />\n        <Text ml=\"2\" fontWeight=\"semibold\">\n          {channel?.user.username}\n        </Text>\n        <Box ml=\"2\" borderRadius=\"50%\" h=\"10px\" w=\"10px\" bg={channel?.user.isOnline ? 'green.500' : 'gray.500'} />\n      </Flex>\n    </GridItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/home/DMSidebar.tsx",
    "content": "import React from 'react';\nimport { GridItem, Box, Text, UnorderedList } from '@chakra-ui/react';\nimport { useQuery } from '@tanstack/react-query';\nimport { AccountBar } from '../AccountBar';\nimport { FriendsListButton } from '../../sections/FriendsListButton';\nimport { DMListItem } from '../../items/DMListItem';\nimport { getUserDMs } from '../../../lib/api/handler/dm';\nimport { dmKey } from '../../../lib/utils/querykeys';\nimport { dmScrollerCss } from './css/dmScrollerCSS';\nimport { useDMSocket } from '../../../lib/api/ws/useDMSocket';\nimport { DMPlaceholder } from '../../sections/DMPlaceholder';\n\nexport const DMSidebar: React.FC = () => {\n  const { data } = useQuery([dmKey], () => getUserDMs().then((result) => result.data));\n\n  useDMSocket();\n\n  return (\n    <GridItem\n      gridColumn=\"2\"\n      gridRow=\"1 / 4\"\n      bg=\"brandGray.dark\"\n      overflowY=\"hidden\"\n      _hover={{ overflowY: 'auto' }}\n      css={dmScrollerCss}\n    >\n      <FriendsListButton />\n      <Text ml=\"4\" textTransform=\"uppercase\" fontSize=\"12px\" fontWeight=\"semibold\" color=\"brandGray.accent\">\n        DIRECT MESSAGES\n      </Text>\n      <UnorderedList listStyleType=\"none\" ml=\"0\" mt=\"4\" id=\"dm-list\">\n        {data?.map((dm) => (\n          <DMListItem dm={dm} key={dm.id} />\n        ))}\n        {data?.length === 0 && (\n          <Box>\n            <DMPlaceholder />\n            <DMPlaceholder />\n            <DMPlaceholder />\n            <DMPlaceholder />\n            <DMPlaceholder />\n          </Box>\n        )}\n      </UnorderedList>\n      <AccountBar />\n    </GridItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/home/css/dmScrollerCSS.ts",
    "content": "export const dmScrollerCss = {\n  '&::-webkit-scrollbar': {\n    width: '4px',\n  },\n  '&::-webkit-scrollbar-track': {\n    width: '4px',\n  },\n  '&::-webkit-scrollbar-thumb': {\n    background: 'brandGray.darker',\n    borderRadius: '18px',\n  },\n};\n"
  },
  {
    "path": "web/src/components/layouts/home/dashboard/FriendsDashboard.tsx",
    "content": "import React from 'react';\nimport { GridItem } from '@chakra-ui/react';\nimport { FriendsListHeader } from './FriendsListHeader';\nimport { FriendsList } from './FriendsList';\nimport { PendingList } from './PendingList';\nimport { scrollbarCss } from '../../../../lib/utils/theme';\nimport { homeStore } from '../../../../lib/stores/homeStore';\n\nexport const FriendsDashboard: React.FC = () => {\n  const isPending = homeStore((state) => state.isPending);\n\n  return (\n    <>\n      <FriendsListHeader />\n      <GridItem\n        gridColumn={3}\n        gridRow=\"2\"\n        bg=\"brandGray.light\"\n        mr=\"5px\"\n        display=\"flex\"\n        overflowY=\"auto\"\n        css={scrollbarCss}\n      >\n        {isPending ? <PendingList /> : <FriendsList />}\n      </GridItem>\n    </>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/home/dashboard/FriendsList.tsx",
    "content": "import React from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { Flex, Text, UnorderedList } from '@chakra-ui/react';\nimport { fKey } from '../../../../lib/utils/querykeys';\nimport { getFriends } from '../../../../lib/api/handler/account';\nimport { OnlineLabel } from '../../../sections/OnlineLabel';\nimport { FriendsListItem } from '../../../items/FriendsListItem';\nimport { useFriendSocket } from '../../../../lib/api/ws/useFriendSocket';\n\nexport const FriendsList: React.FC = () => {\n  const { data } = useQuery([fKey], () => getFriends().then((response) => response.data));\n\n  useFriendSocket();\n\n  if (!data) return null;\n\n  if (data.length === 0) {\n    return (\n      <Flex justify=\"center\" align=\"center\" w=\"full\">\n        <Text textColor=\"brandGray.accent\">No one here yet</Text>\n      </Flex>\n    );\n  }\n\n  return (\n    <UnorderedList listStyleType=\"none\" ml=\"0\" w=\"full\" mt=\"2\" id=\"friend-list\">\n      <OnlineLabel label={`friends — ${data?.length || 0}`} />\n      {data.map((f) => (\n        <FriendsListItem key={f.id} friend={f} />\n      ))}\n    </UnorderedList>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/home/dashboard/FriendsListHeader.tsx",
    "content": "import React from 'react';\nimport { Button, Flex, GridItem, Icon, LightMode, Text, useDisclosure } from '@chakra-ui/react';\nimport { FiUsers } from 'react-icons/fi';\nimport { AddFriendModal } from '../../../modals/AddFriendModal';\nimport { homeStore } from '../../../../lib/stores/homeStore';\nimport { PingIcon } from '../../../common/NotificationIcon';\n\nexport const FriendsListHeader: React.FC = () => {\n  const { isOpen, onOpen, onClose } = useDisclosure();\n  const toggle = homeStore((state) => state.toggleDisplay);\n  const isPending = homeStore((state) => state.isPending);\n  const requests = homeStore((state) => state.requestCount);\n\n  return (\n    <GridItem gridColumn={3} gridRow=\"1\" bg=\"brandGray.light\" padding=\"10px\" zIndex=\"2\" boxShadow=\"md\">\n      <Flex align=\"center\" justify=\"space-between\">\n        <Flex align=\"center\" ml={2} fontSize=\"14px\">\n          <Icon as={FiUsers} fontSize=\"20px\" />\n          <Text ml=\"2\" fontWeight=\"semibold\">\n            Friends\n          </Text>\n          <Button\n            fontSize=\"14px\"\n            ml=\"4\"\n            size=\"xs\"\n            colorScheme=\"gray\"\n            onClick={() => {\n              if (isPending) toggle();\n            }}\n            variant={!isPending ? 'solid' : 'ghost'}\n            _focus={{ boxShadow: 'none' }}\n          >\n            Friends\n          </Button>\n          <Button\n            fontSize=\"14px\"\n            size=\"xs\"\n            ml=\"2\"\n            colorScheme=\"gray\"\n            variant={isPending ? 'solid' : 'ghost'}\n            onClick={() => {\n              if (!isPending) toggle();\n            }}\n            _focus={{ boxShadow: 'none' }}\n          >\n            Pending\n            {requests > 0 && <PingIcon count={requests} />}\n          </Button>\n        </Flex>\n        <LightMode>\n          <Button\n            fontSize=\"14px\"\n            size=\"xs\"\n            bg=\"brandGreen\"\n            _hover={{ bg: 'brandGreen' }}\n            _active={{ bg: 'brandGreen' }}\n            onClick={onOpen}\n          >\n            Add Friend\n          </Button>\n        </LightMode>\n      </Flex>\n      {isOpen && <AddFriendModal isOpen={isOpen} onClose={onClose} />}\n    </GridItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/layouts/home/dashboard/PendingList.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { Flex, UnorderedList, Text } from '@chakra-ui/react';\nimport { rKey } from '../../../../lib/utils/querykeys';\nimport { getPendingRequests } from '../../../../lib/api/handler/account';\nimport { OnlineLabel } from '../../../sections/OnlineLabel';\nimport { RequestListItem } from '../../../items/RequestListItem';\nimport { homeStore } from '../../../../lib/stores/homeStore';\nimport { useRequestSocket } from '../../../../lib/api/ws/useRequestSocket';\n\nexport const PendingList: React.FC = () => {\n  const { data } = useQuery([rKey], () => getPendingRequests().then((response) => response.data), {\n    staleTime: 0,\n  });\n\n  useRequestSocket();\n\n  const reset = homeStore((state) => state.resetRequest);\n\n  useEffect(() => {\n    reset();\n  });\n\n  if (!data) return null;\n\n  if (data.length === 0) {\n    return (\n      <Flex justify=\"center\" align=\"center\" w=\"full\">\n        <Text textColor=\"brandGray.accent\">There are no pending friend requests</Text>\n      </Flex>\n    );\n  }\n\n  return (\n    <UnorderedList listStyleType=\"none\" ml=\"0\" w=\"full\" mt=\"2\">\n      <OnlineLabel label={`Pending — ${data?.length || 0}`} />\n      {data.map((r) => (\n        <RequestListItem request={r} key={r.id} />\n      ))}\n    </UnorderedList>\n  );\n};\n"
  },
  {
    "path": "web/src/components/menus/GuildMenu.tsx",
    "content": "import React from 'react';\nimport { Flex, GridItem, Heading, Icon, Menu, MenuButton, MenuDivider, useDisclosure } from '@chakra-ui/react';\nimport { FiChevronDown, FiX } from 'react-icons/fi';\nimport { FaUserEdit, FaUserPlus } from 'react-icons/fa';\nimport { MdAddCircle } from 'react-icons/md';\nimport { HiLogout } from 'react-icons/hi';\nimport { RiSettings5Fill } from 'react-icons/ri';\nimport { useNavigate, useParams } from 'react-router-dom';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { StyledMenuList } from './StyledMenuList';\nimport { StyledMenuItem, StyledRedMenuItem } from './StyledMenuItem';\nimport { leaveGuild } from '../../lib/api/handler/guilds';\nimport { userStore } from '../../lib/stores/userStore';\nimport { useGetCurrentGuild } from '../../lib/utils/hooks/useGetCurrentGuild';\nimport { GuildSettingsModal } from '../modals/GuildSettingsModal';\nimport { EditMemberModal } from '../modals/EditMemberModal';\nimport { gKey } from '../../lib/utils/querykeys';\nimport { RouterProps } from '../../lib/models/routerProps';\nimport { Guild } from '../../lib/models/guild';\n\ninterface GuildMenuProps {\n  channelOpen: () => void;\n  inviteOpen: () => void;\n}\n\nexport const GuildMenu: React.FC<GuildMenuProps> = ({ channelOpen, inviteOpen }) => {\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n  const guild = useGetCurrentGuild(guildId);\n  const navigate = useNavigate();\n  const cache = useQueryClient();\n\n  const user = userStore((state) => state.current);\n  const isOwner = guild?.ownerId === user?.id;\n\n  const { isOpen, onOpen, onClose } = useDisclosure();\n  const { isOpen: memberOpen, onOpen: memberOnOpen, onClose: memberOnClose } = useDisclosure();\n\n  const handleLeave = async (): Promise<void> => {\n    try {\n      const { data } = await leaveGuild(guildId);\n      if (data) {\n        cache.setQueryData<Guild[]>([gKey], (d) => d?.filter((g) => g.id !== guild?.id) ?? []);\n        navigate('/channels/me', { replace: true });\n      }\n    } catch (err) {}\n  };\n\n  return (\n    <GridItem gridColumn={2} gridRow=\"1\" bg=\"brandGray.light\" padding=\"10px\" zIndex=\"2\" boxShadow=\"md\">\n      <Menu placement=\"bottom-end\" isLazy>\n        {({ isOpen: menuIsOpen }) => (\n          <>\n            <Flex justify=\"space-between\" align=\"center\">\n              <Heading fontSize=\"20px\" noOfLines={1}>\n                {guild?.name}\n              </Heading>\n              <MenuButton>\n                <Icon as={!menuIsOpen ? FiChevronDown : FiX} />\n              </MenuButton>\n            </Flex>\n            <StyledMenuList>\n              <StyledMenuItem label=\"Invite People\" icon={FaUserPlus} handleClick={inviteOpen} />\n              {isOwner && <StyledMenuItem label=\"Server Settings\" icon={RiSettings5Fill} handleClick={onOpen} />}\n              {isOwner && <StyledMenuItem label=\"Create Channel\" icon={MdAddCircle} handleClick={channelOpen} />}\n              <MenuDivider />\n              <StyledMenuItem label=\"Change Appearance\" icon={FaUserEdit} handleClick={memberOnOpen} />\n              {!isOwner && (\n                <>\n                  <MenuDivider />\n                  <StyledRedMenuItem label=\"Leave Server\" icon={HiLogout} handleClick={handleLeave} />\n                </>\n              )}\n            </StyledMenuList>\n          </>\n        )}\n      </Menu>\n      {isOpen && <GuildSettingsModal guildId={guildId} isOpen={isOpen} onClose={onClose} />}\n      {memberOpen && <EditMemberModal guildId={guildId} isOpen={memberOpen} onClose={memberOnClose} />}\n    </GridItem>\n  );\n};\n"
  },
  {
    "path": "web/src/components/menus/MemberContextMenu.tsx",
    "content": "import React, { useState } from 'react';\nimport { Divider, Flex, Text, useDisclosure } from '@chakra-ui/react';\nimport { Item, Menu, theme } from 'react-contexify';\nimport { useNavigate } from 'react-router-dom';\nimport { getOrCreateDirectMessage } from '../../lib/api/handler/dm';\nimport { sendFriendRequest } from '../../lib/api/handler/account';\nimport { RemoveFriendModal } from '../modals/RemoveFriendModal';\nimport { ModActionModal } from '../modals/ModActionModal';\nimport { Member } from '../../lib/models/member';\n\ninterface MemberContextMenuProps {\n  member: Member;\n  isOwner: boolean;\n  id: string;\n}\n\nexport const MemberContextMenu: React.FC<MemberContextMenuProps> = ({ member, isOwner, id }) => {\n  const navigate = useNavigate();\n  const { isOpen, onOpen, onClose } = useDisclosure();\n  const { isOpen: modIsOpen, onOpen: modOnOpen, onClose: modOnClose } = useDisclosure();\n  const [isBan, setIsBan] = useState(false);\n\n  const getOrCreateDM = async (): Promise<void> => {\n    try {\n      const { data } = await getOrCreateDirectMessage(member.id);\n      if (data) {\n        navigate(`/channels/me/${data.id}`);\n      }\n    } catch (err) {}\n  };\n\n  const handleFriendClick = async (): Promise<void> => {\n    if (!member.isFriend) {\n      try {\n        await sendFriendRequest(member.id);\n      } catch (err) {}\n    } else {\n      onOpen();\n    }\n  };\n\n  return (\n    <>\n      <Menu id={id} theme={theme.dark}>\n        <Item onClick={() => getOrCreateDM()} className=\"menu-item\">\n          <Flex align=\"center\" justify=\"space-between\" w=\"full\">\n            <Text>Message</Text>\n          </Flex>\n        </Item>\n        <Item onClick={handleFriendClick} className=\"menu-item\">\n          <Flex align=\"center\" justify=\"space-between\" w=\"full\">\n            <Text>{member.isFriend ? 'Remove' : 'Add'} Friend</Text>\n          </Flex>\n        </Item>\n        {isOwner && (\n          <>\n            <Flex align=\"center\" justify=\"center\" w=\"full\">\n              <Divider my=\"1\" w=\"90%\" />\n            </Flex>\n            <Item\n              onClick={() => {\n                setIsBan(false);\n                modOnOpen();\n              }}\n              className=\"delete-item\"\n            >\n              <Flex align=\"center\" justify=\"space-between\" w=\"full\" textOverflow=\"ellipsis\">\n                <Text noOfLines={1}>Kick {member.username}</Text>\n              </Flex>\n            </Item>\n            <Item\n              onClick={() => {\n                setIsBan(true);\n                modOnOpen();\n              }}\n              className=\"delete-item\"\n            >\n              <Flex align=\"center\" justify=\"space-between\" w=\"full\">\n                <Text>Ban {member.username}</Text>\n              </Flex>\n            </Item>\n          </>\n        )}\n      </Menu>\n      {isOpen && <RemoveFriendModal member={member} isOpen onClose={onClose} />}\n      {modIsOpen && <ModActionModal member={member} isOpen={modIsOpen} isBan={isBan} onClose={modOnClose} />}\n    </>\n  );\n};\n"
  },
  {
    "path": "web/src/components/menus/StyledMenuItem.tsx",
    "content": "import React from 'react';\nimport { Flex, Icon, MenuItem, Text } from '@chakra-ui/react';\nimport { IconType } from 'react-icons';\n\ninterface StyledMenuItemProps {\n  label: string;\n  icon: IconType;\n  handleClick: () => void;\n}\n\nexport const StyledMenuItem: React.FC<StyledMenuItemProps> = ({ label, icon, handleClick }) => (\n  <MenuItem _hover={{ bg: 'highlight.standard', borderRadius: '2px' }} onClick={handleClick}>\n    <Flex align=\"center\" justify=\"space-between\" w=\"full\">\n      <Text>{label}</Text>\n      <Icon as={icon} />\n    </Flex>\n  </MenuItem>\n);\n\nexport const StyledRedMenuItem: React.FC<StyledMenuItemProps> = ({ label, icon, handleClick }) => (\n  <MenuItem _hover={{ bg: 'menuRed', color: '#fff', borderRadius: '2px' }} onClick={handleClick}>\n    <Flex align=\"center\" justify=\"space-between\" w=\"full\">\n      <Text>{label}</Text>\n      <Icon as={icon} />\n    </Flex>\n  </MenuItem>\n);\n"
  },
  {
    "path": "web/src/components/menus/StyledMenuList.tsx",
    "content": "import { MenuList } from '@chakra-ui/react';\nimport React from 'react';\n\ninterface IProps {\n  children: React.ReactNode;\n}\n\nexport const StyledMenuList: React.FC<IProps> = ({ children }) => (\n  <MenuList bg=\"brandGray.darkest\" px=\"2\">\n    {children}\n  </MenuList>\n);\n"
  },
  {
    "path": "web/src/components/modals/AddFriendModal.tsx",
    "content": "import React from 'react';\nimport {\n  Button,\n  Input,\n  InputGroup,\n  InputLeftAddon,\n  InputRightElement,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n  useClipboard,\n} from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { userStore } from '../../lib/stores/userStore';\nimport { InputField } from '../common/InputField';\nimport { sendFriendRequest } from '../../lib/api/handler/account';\nimport { rKey } from '../../lib/utils/querykeys';\n\ninterface AddFriendModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport const AddFriendModal: React.FC<AddFriendModalProps> = ({ isOpen, onClose }) => {\n  const current = userStore((state) => state.current);\n  const cache = useQueryClient();\n  const { hasCopied, onCopy } = useClipboard(current?.id || '');\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n      <ModalContent bg=\"brandGray.light\">\n        <Formik\n          initialValues={{\n            id: '',\n          }}\n          onSubmit={async (values, { setErrors }) => {\n            if (values.id === '' && values.id.length !== 20) {\n              setErrors({ id: 'Enter a valid ID' });\n            } else {\n              try {\n                const { data } = await sendFriendRequest(values.id);\n                if (data) {\n                  onClose();\n                  await cache.invalidateQueries([rKey]);\n                }\n              } catch (err: any) {\n                if (err?.response?.data?.error) {\n                  const error = err?.response?.data?.error?.message;\n                  setErrors({ id: error });\n                }\n              }\n            }\n          }}\n        >\n          {({ isSubmitting }) => (\n            <Form>\n              <ModalHeader fontWeight=\"bold\" pb=\"0\">\n                ADD FRIEND\n              </ModalHeader>\n              <ModalCloseButton _focus={{ outline: 'none' }} />\n              <ModalBody>\n                <Text mb=\"4\">You can add a friend with their UID.</Text>\n                <InputGroup mb={2}>\n                  <InputLeftAddon bg=\"#202225\" borderColor=\"black\">\n                    UID\n                  </InputLeftAddon>\n                  <Input\n                    bg=\"brandGray.dark\"\n                    borderColor={hasCopied ? 'brandGreen' : 'black'}\n                    borderRadius=\"3px\"\n                    focusBorderColor=\"highlight.standard\"\n                    value={current?.id || ''}\n                    isReadOnly\n                  />\n                  <InputRightElement width=\"4.5rem\">\n                    <Button\n                      h=\"1.75rem\"\n                      size=\"sm\"\n                      bg={hasCopied ? 'brandGreen' : 'highlight.standard'}\n                      color=\"white\"\n                      _hover={{ bg: 'highlight.hover' }}\n                      _active={{ bg: 'highlight.active' }}\n                      _focus={{ boxShadow: 'none' }}\n                      onClick={onCopy}\n                    >\n                      {hasCopied ? 'Copied' : 'Copy'}\n                    </Button>\n                  </InputRightElement>\n                </InputGroup>\n\n                <InputField label=\"Enter a user ID\" name=\"id\" />\n              </ModalBody>\n              <ModalFooter bg=\"brandGray.dark\" mt=\"2\">\n                <Button mr={6} variant=\"link\" onClick={onClose} fontSize=\"14px\" _focus={{ outline: 'none' }}>\n                  Cancel\n                </Button>\n                <Button\n                  background=\"highlight.standard\"\n                  color=\"white\"\n                  type=\"submit\"\n                  _hover={{ bg: 'highlight.hover' }}\n                  _active={{ bg: 'highlight.active' }}\n                  _focus={{ boxShadow: 'none' }}\n                  isLoading={isSubmitting}\n                  fontSize=\"14px\"\n                >\n                  Send Friend Request\n                </Button>\n              </ModalFooter>\n            </Form>\n          )}\n        </Formik>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/AddGuildModal.tsx",
    "content": "import {\n  Button,\n  Divider,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n  VStack,\n} from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React, { useState } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useNavigate } from 'react-router-dom';\nimport { InputField } from '../common/InputField';\nimport { GuildSchema } from '../../lib/utils/validation/guild.schema';\nimport { createGuild, joinGuild } from '../../lib/api/handler/guilds';\nimport { userStore } from '../../lib/stores/userStore';\nimport { toErrorMap } from '../../lib/utils/toErrorMap';\nimport { gKey } from '../../lib/utils/querykeys';\nimport { Guild } from '../../lib/models/guild';\n\ninterface IProps {\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nenum AddGuildScreen {\n  START,\n  INVITE,\n  CREATE,\n}\n\nexport const AddGuildModal: React.FC<IProps> = ({ isOpen, onClose }) => {\n  const [screen, setScreen] = useState(AddGuildScreen.START);\n\n  const goBack = (): void => setScreen(AddGuildScreen.START);\n  const submitClose = (): void => {\n    setScreen(AddGuildScreen.START);\n    onClose();\n  };\n\n  return (\n    <Modal isOpen={isOpen} onClose={submitClose} isCentered size=\"sm\">\n      <ModalOverlay />\n\n      {screen === AddGuildScreen.INVITE && <JoinServerModal goBack={goBack} submitClose={submitClose} />}\n      {screen === AddGuildScreen.CREATE && <CreateServerModal goBack={goBack} submitClose={submitClose} />}\n      {screen === AddGuildScreen.START && (\n        <ModalContent bg=\"brandGray.light\">\n          <ModalHeader textAlign=\"center\" fontWeight=\"bold\">\n            Create a server\n          </ModalHeader>\n          <ModalCloseButton _focus={{ outline: 'none' }} />\n          <ModalBody pb={6}>\n            <VStack spacing=\"5\">\n              <Text textAlign=\"center\">\n                Your server is where you and your friends hang out. Make yours and start talking.\n              </Text>\n\n              <Button\n                background=\"highlight.standard\"\n                color=\"white\"\n                type=\"submit\"\n                _hover={{ bg: 'highlight.hover' }}\n                _active={{ bg: 'highlight.active' }}\n                _focus={{ boxShadow: 'none' }}\n                w=\"full\"\n                onClick={() => setScreen(AddGuildScreen.CREATE)}\n              >\n                Create My Own\n              </Button>\n\n              <Divider />\n\n              <Text>Have an invite already?</Text>\n\n              <Button\n                mt=\"4\"\n                background=\"highlight.standard\"\n                color=\"white\"\n                type=\"submit\"\n                _hover={{ bg: 'highlight.hover' }}\n                _active={{ bg: 'highlight.active' }}\n                _focus={{ boxShadow: 'none' }}\n                w=\"full\"\n                onClick={() => setScreen(AddGuildScreen.INVITE)}\n              >\n                Join a Server\n              </Button>\n            </VStack>\n          </ModalBody>\n        </ModalContent>\n      )}\n    </Modal>\n  );\n};\n\ninterface IScreenProps {\n  goBack: () => void;\n  submitClose: () => void;\n}\n\nconst JoinServerModal: React.FC<IScreenProps> = ({ goBack, submitClose }) => {\n  const cache = useQueryClient();\n  const navigate = useNavigate();\n\n  return (\n    <ModalContent bg=\"brandGray.light\">\n      <Formik\n        initialValues={{\n          link: '',\n        }}\n        onSubmit={async (values, { setErrors }) => {\n          if (values.link === '') {\n            setErrors({ link: 'Enter a valid link' });\n          } else {\n            try {\n              const { data } = await joinGuild(values);\n              if (data) {\n                cache.setQueryData<Guild[]>([gKey], (old) => [...(old ?? []), data]);\n                submitClose();\n                navigate(`/channels/${data.id}/${data.default_channel_id}`);\n              }\n            } catch (err: any) {\n              const status = err?.response?.status;\n              if (status === 400 || status === 500) {\n                setErrors({ link: err?.response?.data?.error.message });\n              }\n            }\n          }\n        }}\n      >\n        {({ isSubmitting }) => (\n          <Form>\n            <ModalHeader textAlign=\"center\" fontWeight=\"bold\" pb=\"0\">\n              Join a Server\n            </ModalHeader>\n            <ModalCloseButton _focus={{ outline: 'none' }} />\n            <ModalBody pb={3}>\n              <Text fontSize=\"14px\" textColor=\"brandGray.accent\">\n                Enter an invite below to join an existing server\n              </Text>\n              <InputField label=\"invite link\" name=\"link\" />\n\n              <Text mt=\"4\" fontSize=\"12px\" fontWeight=\"semibold\" textColor=\"brandGray.accent\" textTransform=\"uppercase\">\n                invite links should look like\n              </Text>\n\n              <Text mt=\"2\" fontSize=\"12px\" textColor=\"brandGray.accent\">\n                hTKzmak\n              </Text>\n              <Text fontSize=\"12px\" textColor=\"brandGray.accent\">\n                https://valkyrieapp.xyz/hTKzmak\n              </Text>\n            </ModalBody>\n\n            <ModalFooter bg=\"brandGray.dark\">\n              <Button mr={6} variant=\"link\" onClick={goBack} fontSize=\"14px\" _focus={{ outline: 'none' }}>\n                Back\n              </Button>\n              <Button\n                background=\"highlight.standard\"\n                color=\"white\"\n                type=\"submit\"\n                _hover={{ bg: 'highlight.hover' }}\n                _active={{ bg: 'highlight.active' }}\n                _focus={{ boxShadow: 'none' }}\n                isLoading={isSubmitting}\n                fontSize=\"14px\"\n              >\n                Join Server\n              </Button>\n            </ModalFooter>\n          </Form>\n        )}\n      </Formik>\n    </ModalContent>\n  );\n};\n\nconst CreateServerModal: React.FC<IScreenProps> = ({ goBack, submitClose }) => {\n  const user = userStore((state) => state.current);\n  const cache = useQueryClient();\n  const navigate = useNavigate();\n\n  return (\n    <ModalContent bg=\"brandGray.light\">\n      <Formik\n        initialValues={{\n          name: `${user?.username}'s server`,\n        }}\n        validationSchema={GuildSchema}\n        onSubmit={async (values, { setErrors }) => {\n          try {\n            const { data } = await createGuild(values);\n            if (data) {\n              cache.setQueryData<Guild[]>([gKey], (old) => [...(old ?? []), data]);\n              submitClose();\n              navigate(`/channels/${data.id}/${data.default_channel_id}`);\n            }\n          } catch (err: any) {\n            if (err?.response?.status === 400) {\n              setErrors({ name: 'The server limit is 100' });\n            }\n            if (err?.response?.data?.errors) {\n              const errors = err?.response?.data?.errors;\n              setErrors(toErrorMap(errors));\n            }\n          }\n        }}\n      >\n        {({ isSubmitting, values }) => (\n          <Form>\n            <ModalHeader textAlign=\"center\" fontWeight=\"bold\" pb=\"0\">\n              Create your server\n            </ModalHeader>\n            <ModalCloseButton _focus={{ outline: 'none' }} />\n            <ModalBody pb={3}>\n              <InputField label=\"server name\" name=\"name\" value={values.name} />\n            </ModalBody>\n\n            <ModalFooter bg=\"brandGray.dark\">\n              <Button mr={6} fontSize=\"14px\" variant=\"link\" onClick={goBack} _focus={{ outline: 'none' }}>\n                Back\n              </Button>\n              <Button\n                background=\"highlight.standard\"\n                color=\"white\"\n                type=\"submit\"\n                _hover={{ bg: 'highlight.hover' }}\n                _active={{ bg: 'highlight.active' }}\n                _focus={{ boxShadow: 'none' }}\n                isLoading={isSubmitting}\n                fontSize=\"14px\"\n              >\n                Create\n              </Button>\n            </ModalFooter>\n          </Form>\n        )}\n      </Formik>\n    </ModalContent>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/ChangePasswordModal.tsx",
    "content": "import {\n  Button,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n  useToast,\n} from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React from 'react';\nimport { toErrorMap } from '../../lib/utils/toErrorMap';\nimport { ChangePasswordSchema } from '../../lib/utils/validation/auth.schema';\nimport { InputField } from '../common/InputField';\nimport { changePassword } from '../../lib/api/handler/auth';\n\ninterface IProps {\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport const ChangePasswordModal: React.FC<IProps> = ({ isOpen, onClose }) => {\n  const toast = useToast();\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n\n      <ModalContent bg=\"brandGray.light\">\n        <Formik\n          initialValues={{\n            currentPassword: '',\n            newPassword: '',\n            confirmNewPassword: '',\n          }}\n          validationSchema={ChangePasswordSchema}\n          onSubmit={async (values, { setErrors }) => {\n            try {\n              const { data } = await changePassword(values);\n              if (data) {\n                toast({\n                  title: 'Changed Password',\n                  status: 'success',\n                  duration: 5000,\n                  isClosable: true,\n                });\n                onClose();\n              }\n            } catch (err: any) {\n              if (err?.response?.status === 500) {\n                toast({\n                  title: 'Server Error',\n                  description: 'Try again later',\n                  status: 'error',\n                  duration: 3000,\n                  isClosable: true,\n                });\n              }\n              if (err?.response?.data?.errors) {\n                const errors = err?.response?.data?.errors;\n                setErrors(toErrorMap(errors));\n              }\n            }\n          }}\n        >\n          {({ isSubmitting }) => (\n            <Form>\n              <ModalHeader textAlign=\"center\" fontWeight=\"bold\">\n                Change your password\n              </ModalHeader>\n              <ModalCloseButton _focus={{ outline: 'none' }} />\n              <ModalBody pb={6}>\n                <Text>Enter your current password and a new password</Text>\n                <InputField label=\"current password\" name=\"currentPassword\" autoComplete=\"password\" type=\"password\" />\n\n                <InputField label=\"new password\" name=\"newPassword\" autoComplete=\"password\" type=\"password\" />\n\n                <InputField\n                  label=\"confirm new password\"\n                  name=\"confirmNewPassword\"\n                  autoComplete=\"password\"\n                  type=\"password\"\n                />\n              </ModalBody>\n\n              <ModalFooter bg=\"brandGray.dark\">\n                <Button onClick={onClose} fontSize=\"14px\" mr={6} variant=\"link\" _focus={{ outline: 'none' }}>\n                  Cancel\n                </Button>\n                <Button\n                  background=\"highlight.standard\"\n                  color=\"white\"\n                  type=\"submit\"\n                  _hover={{ bg: 'highlight.hover' }}\n                  _active={{ bg: 'highlight.active' }}\n                  _focus={{ boxShadow: 'none' }}\n                  isLoading={isSubmitting}\n                  fontSize=\"14px\"\n                >\n                  Done\n                </Button>\n              </ModalFooter>\n            </Form>\n          )}\n        </Formik>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/ChannelSettingsModal.tsx",
    "content": "import {\n  Avatar,\n  Box,\n  Button,\n  Divider,\n  Flex,\n  FormControl,\n  FormLabel,\n  LightMode,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Switch,\n  Text,\n} from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React, { useState } from 'react';\nimport { AiOutlineLock } from 'react-icons/ai';\nimport { FaRegTrashAlt } from 'react-icons/fa';\nimport { CUIAutoComplete } from 'chakra-ui-autocomplete';\nimport { useQuery } from '@tanstack/react-query';\nimport { InputField } from '../common/InputField';\nimport { toErrorMap } from '../../lib/utils/toErrorMap';\nimport { getGuildMembers } from '../../lib/api/handler/guilds';\nimport { ChannelSchema } from '../../lib/utils/validation/channel.schema';\nimport { useGetCurrentChannel } from '../../lib/utils/hooks/useGetCurrentChannel';\nimport { mKey } from '../../lib/utils/querykeys';\nimport { deleteChannel, editChannel, getPrivateChannelMembers } from '../../lib/api/handler/channel';\n\ninterface IProps {\n  guildId: string;\n  channelId: string;\n  isOpen: boolean;\n  onClose: () => void;\n}\n\ninterface Item {\n  // eslint-disable-next-line react/no-unused-prop-types\n  value: string;\n  label: string;\n  image: string;\n}\n\nenum ChannelScreen {\n  START,\n  CONFIRM,\n}\n\nconst ListItem = ({ image, label }: Item): JSX.Element => (\n  <Flex align=\"center\">\n    <Avatar mr={2} size=\"sm\" src={image} />\n    <Text textColor=\"#000\">{label}</Text>\n  </Flex>\n);\n\nexport const ChannelSettingsModal: React.FC<IProps> = ({ guildId, channelId, isOpen, onClose }) => {\n  const key = [mKey, guildId];\n  const { data } = useQuery(key, () => getGuildMembers(guildId).then((response) => response.data));\n\n  const channel = useGetCurrentChannel(channelId, guildId);\n\n  const members: Item[] = [];\n  const [selectedItems, setSelectedItems] = useState<Item[]>([]);\n  const [screen, setScreen] = useState(ChannelScreen.START);\n  const [showError, toggleShow] = useState(false);\n\n  const goBack = (): void => setScreen(ChannelScreen.START);\n  const submitClose = (): void => {\n    setScreen(ChannelScreen.START);\n    onClose();\n  };\n\n  data?.map((m) =>\n    members.push({\n      label: m.username,\n      value: m.id,\n      image: m.image,\n    })\n  );\n\n  // eslint-disable-next-line\n  const { data: _ } = useQuery<Item[]>(['pcmembers', channelId], async () => {\n    const { data: memberData } = await getPrivateChannelMembers(channelId);\n    const current = members.filter((m) => memberData.includes(m.value));\n    setSelectedItems(current);\n    return current;\n  });\n\n  const handleCreateItem = (item: Item): void => {\n    setSelectedItems((curr) => [...curr, item]);\n  };\n\n  const handleSelectedItemsChange = (changedItems?: Item[]): void => {\n    if (changedItems) {\n      setSelectedItems(changedItems);\n    }\n  };\n\n  if (!channel) return null;\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n      {screen === ChannelScreen.START && (\n        <ModalContent bg=\"brandGray.light\">\n          <Formik\n            initialValues={{\n              name: channel.name,\n              isPublic: channel.isPublic,\n            }}\n            validationSchema={ChannelSchema}\n            onSubmit={async (values, { setErrors, resetForm }) => {\n              try {\n                const ids: string[] = [];\n                selectedItems.map((i) => ids.push(i.value));\n                const { data: responseData } = await editChannel(channelId, {\n                  ...values,\n                  members: ids,\n                });\n                if (responseData) {\n                  resetForm();\n                  onClose();\n                }\n              } catch (err: any) {\n                if (err?.response?.status === 500) {\n                  toggleShow(true);\n                }\n                if (err?.response?.data?.errors) {\n                  const errors = err?.response?.data?.errors;\n                  setErrors(toErrorMap(errors));\n                }\n              }\n            }}\n          >\n            {({ isSubmitting, setFieldValue, values }) => (\n              <Form>\n                <ModalHeader textAlign=\"center\" fontWeight=\"bold\">\n                  Channel Settings\n                </ModalHeader>\n                <ModalCloseButton _focus={{ outline: 'none' }} />\n                <ModalBody>\n                  <InputField label=\"channel name\" name=\"name\" />\n\n                  <FormControl display=\"flex\" alignItems=\"center\" justifyContent=\"space-between\" mt=\"4\">\n                    <FormLabel mb=\"0\">\n                      <Flex align=\"center\">\n                        <AiOutlineLock />\n                        <Text ml=\"2\">Private Channel</Text>\n                      </Flex>\n                    </FormLabel>\n                    <Switch\n                      defaultChecked={!values.isPublic}\n                      onChange={(e) => {\n                        setFieldValue('isPublic', !e.target.checked);\n                      }}\n                    />\n                  </FormControl>\n                  <Text mt=\"4\" fontSize=\"14px\" textColor=\"brandGray.accent\">\n                    By making a channel private, only selected members will be able to view this channel\n                  </Text>\n                  {!values.isPublic && (\n                    <Box mt=\"2\" pb={0}>\n                      <CUIAutoComplete\n                        label=\"Who can access this channel\"\n                        placeholder=\"\"\n                        onCreateItem={handleCreateItem}\n                        items={members}\n                        selectedItems={selectedItems}\n                        itemRenderer={ListItem}\n                        onSelectedItemsChange={(changes) => handleSelectedItemsChange(changes.selectedItems)}\n                      />\n                    </Box>\n                  )}\n\n                  <Divider my=\"2\" />\n\n                  <LightMode>\n                    <Button\n                      onClick={() => setScreen(ChannelScreen.CONFIRM)}\n                      colorScheme=\"red\"\n                      variant=\"ghost\"\n                      fontSize=\"14px\"\n                      rightIcon={<FaRegTrashAlt />}\n                    >\n                      Delete Channel\n                    </Button>\n                  </LightMode>\n\n                  {showError && (\n                    <Text my=\"2\" color=\"menuRed\" align=\"center\">\n                      Server Error. Try again later\n                    </Text>\n                  )}\n                </ModalBody>\n\n                <ModalFooter bg=\"brandGray.dark\">\n                  <Button onClick={onClose} mr={6} variant=\"link\" fontSize=\"14px\" _focus={{ outline: 'none' }}>\n                    Cancel\n                  </Button>\n                  <Button\n                    background=\"highlight.standard\"\n                    color=\"white\"\n                    type=\"submit\"\n                    _hover={{ bg: 'highlight.hover' }}\n                    _active={{ bg: 'highlight.active' }}\n                    _focus={{ boxShadow: 'none' }}\n                    isLoading={isSubmitting}\n                    fontSize=\"14px\"\n                  >\n                    Save Changes\n                  </Button>\n                </ModalFooter>\n              </Form>\n            )}\n          </Formik>\n        </ModalContent>\n      )}\n      {screen === ChannelScreen.CONFIRM && (\n        <DeleteChannelModal goBack={goBack} submitClose={submitClose} name={channel.name} channelId={channel.id} />\n      )}\n    </Modal>\n  );\n};\n\ninterface IScreenProps {\n  goBack: () => void;\n  submitClose: () => void;\n  name: string;\n  channelId: string;\n}\n\nconst DeleteChannelModal: React.FC<IScreenProps> = ({ goBack, submitClose, name, channelId }) => {\n  const [showError, toggleShow] = useState(false);\n  const handleDelete = async (): Promise<void> => {\n    try {\n      const { data } = await deleteChannel(channelId);\n      if (data) {\n        submitClose();\n      }\n    } catch (err) {\n      toggleShow(true);\n    }\n  };\n\n  return (\n    <ModalContent bg=\"brandGray.light\">\n      <ModalHeader fontWeight=\"bold\" pb=\"0\">\n        Delete Channel\n      </ModalHeader>\n      <ModalBody pb={3}>\n        <Text>Are you sure you want to delete #{name}? This cannot be undone.</Text>\n\n        {showError && (\n          <Text my=\"2\" color=\"menuRed\" align=\"center\">\n            Server Error. Try again later\n          </Text>\n        )}\n      </ModalBody>\n\n      <ModalFooter bg=\"brandGray.dark\">\n        <Button mr={6} variant=\"link\" onClick={goBack} fontSize=\"14px\" _focus={{ outline: 'none' }}>\n          Cancel\n        </Button>\n        <LightMode>\n          <Button colorScheme=\"red\" fontSize=\"14px\" onClick={() => handleDelete()}>\n            Delete Channel\n          </Button>\n        </LightMode>\n      </ModalFooter>\n    </ModalContent>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/CreateChannelModal.tsx",
    "content": "import {\n  Avatar,\n  Box,\n  Button,\n  Flex,\n  FormControl,\n  FormLabel,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Switch,\n  Text,\n} from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React, { useState } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { AiOutlineLock } from 'react-icons/ai';\nimport { CUIAutoComplete } from 'chakra-ui-autocomplete';\nimport { useNavigate } from 'react-router-dom';\nimport { InputField } from '../common/InputField';\nimport { toErrorMap } from '../../lib/utils/toErrorMap';\nimport { getGuildMembers } from '../../lib/api/handler/guilds';\nimport { ChannelSchema } from '../../lib/utils/validation/channel.schema';\nimport { mKey } from '../../lib/utils/querykeys';\nimport { createChannel } from '../../lib/api/handler/channel';\n\ninterface IProps {\n  guildId: string;\n  isOpen: boolean;\n  onClose: () => void;\n}\n\ninterface Item {\n  // eslint-disable-next-line react/no-unused-prop-types\n  value: string;\n  label: string;\n  image: string;\n}\n\nconst ListItem = ({ image, label }: Item): JSX.Element => (\n  <Flex align=\"center\">\n    <Avatar mr={2} size=\"sm\" src={image} />\n    <Text textColor=\"#000\">{label}</Text>\n  </Flex>\n);\n\nexport const CreateChannelModal: React.FC<IProps> = ({ guildId, isOpen, onClose }) => {\n  const key = [mKey, guildId];\n  const navigate = useNavigate();\n  const { data } = useQuery(key, () => getGuildMembers(guildId).then((response) => response.data));\n  const [showError, toggleError] = useState(false);\n\n  const members: Item[] = [];\n  const [selectedItems, setSelectedItems] = useState<Item[]>([]);\n\n  data?.map((m) =>\n    members.push({\n      label: m.username,\n      value: m.id,\n      image: m.image,\n    })\n  );\n\n  const handleCreateItem = (item: Item): void => {\n    setSelectedItems((curr) => [...curr, item]);\n  };\n\n  const handleSelectedItemsChange = (changedItems?: Item[]): void => {\n    if (changedItems) {\n      setSelectedItems(changedItems);\n    }\n  };\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n      <ModalContent bg=\"brandGray.light\">\n        <Formik\n          initialValues={{\n            name: '',\n            isPublic: true,\n          }}\n          validationSchema={ChannelSchema}\n          onSubmit={async (values, { setErrors, resetForm }) => {\n            try {\n              const ids: string[] = [];\n              selectedItems.map((i) => ids.push(i.value));\n              const { data: responseData } = await createChannel(guildId, {\n                ...values,\n                members: ids,\n              });\n              if (responseData) {\n                resetForm();\n                onClose();\n                navigate(`/channels/${guildId}/${responseData.id}`);\n              }\n            } catch (err: any) {\n              if (err?.response?.status === 500) {\n                toggleError(true);\n              }\n              if (err?.response?.data?.errors) {\n                const errors = err?.response?.data?.errors;\n                setErrors(toErrorMap(errors));\n              }\n            }\n          }}\n        >\n          {({ isSubmitting, setFieldValue, values }) => (\n            <Form>\n              <ModalHeader textAlign=\"center\" fontWeight=\"bold\">\n                Create Text Channel\n              </ModalHeader>\n              <ModalCloseButton _focus={{ outline: 'none' }} />\n              <ModalBody>\n                <InputField label=\"channel name\" name=\"name\" />\n\n                <FormControl display=\"flex\" alignItems=\"center\" justifyContent=\"space-between\" mt=\"4\">\n                  <FormLabel htmlFor=\"email-alerts\" mb=\"0\">\n                    <Flex align=\"center\">\n                      <AiOutlineLock />\n                      <Text ml=\"2\">Private Channel</Text>\n                    </Flex>\n                  </FormLabel>\n                  <Switch\n                    onChange={(e) => {\n                      setFieldValue('isPublic', !e.target.checked);\n                    }}\n                  />\n                </FormControl>\n                <Text mt=\"4\" fontSize=\"14px\" textColor=\"brandGray.accent\">\n                  By making a channel private, only selected members will be able to view this channel\n                </Text>\n                {!values.isPublic && (\n                  <Box mt=\"2\" pb={0}>\n                    <CUIAutoComplete\n                      label=\"Who can access this channel\"\n                      placeholder=\"\"\n                      onCreateItem={handleCreateItem}\n                      items={members}\n                      selectedItems={selectedItems}\n                      itemRenderer={ListItem}\n                      onSelectedItemsChange={(changes) => handleSelectedItemsChange(changes.selectedItems)}\n                    />\n                  </Box>\n                )}\n\n                {showError && (\n                  <Text my=\"2\" color=\"menuRed\" align=\"center\">\n                    Server Error. Try again later\n                  </Text>\n                )}\n              </ModalBody>\n\n              <ModalFooter bg=\"brandGray.dark\">\n                <Button onClick={onClose} fontSize=\"14px\" mr={6} variant=\"link\" _focus={{ outline: 'none' }}>\n                  Cancel\n                </Button>\n                <Button\n                  background=\"highlight.standard\"\n                  color=\"white\"\n                  type=\"submit\"\n                  fontSize=\"14px\"\n                  _hover={{ bg: 'highlight.hover' }}\n                  _active={{ bg: 'highlight.active' }}\n                  _focus={{ boxShadow: 'none' }}\n                  isLoading={isSubmitting}\n                >\n                  Create Channel\n                </Button>\n              </ModalFooter>\n            </Form>\n          )}\n        </Formik>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/CropImageModal.tsx",
    "content": "import {\n  Box,\n  Button,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Slider,\n  SliderFilledTrack,\n  SliderThumb,\n  SliderTrack,\n} from '@chakra-ui/react';\nimport React, { useCallback, useState } from 'react';\nimport Cropper from 'react-easy-crop';\nimport getCroppedImg from '../../lib/utils/cropImage';\n\ninterface IProps {\n  isOpen: boolean;\n  initialImage: string;\n  applyCrop: (image: Blob) => void;\n  onClose: () => void;\n}\n\nexport const CropImageModal: React.FC<IProps> = ({ isOpen, onClose, applyCrop, initialImage }) => {\n  const [crop, setCrop] = useState({\n    x: 0,\n    y: 0,\n  });\n  const [zoom, setZoom] = useState(1);\n  const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);\n\n  const onCropComplete = useCallback((_: any, croppedAreaPixelsResult: any) => {\n    setCroppedAreaPixels(croppedAreaPixelsResult);\n  }, []);\n\n  const showCroppedImage = useCallback(async () => {\n    try {\n      const croppedImage = await getCroppedImg(initialImage, croppedAreaPixels);\n      applyCrop(croppedImage);\n    } catch (e) {}\n  }, [croppedAreaPixels, initialImage, applyCrop]);\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered closeOnOverlayClick={false}>\n      <ModalOverlay />\n\n      <ModalContent bg=\"brandGray.light\">\n        <ModalHeader fontWeight=\"bold\">EDIT MEDIA</ModalHeader>\n        <ModalCloseButton _focus={{ outline: 'none' }} />\n        <ModalBody>\n          <Box h=\"400px\" overflow=\"hidden\" position=\"relative\">\n            <Cropper\n              image={initialImage}\n              crop={crop}\n              zoom={zoom}\n              aspect={1}\n              cropShape=\"round\"\n              onCropChange={setCrop}\n              onCropComplete={onCropComplete}\n              onZoomChange={setZoom}\n            />\n          </Box>\n          <Slider\n            aria-label=\"zoom\"\n            min={1}\n            max={3}\n            step={0.01}\n            value={zoom}\n            onChange={(value: number) => setZoom(value)}\n            my=\"4\"\n          >\n            <SliderTrack>\n              <SliderFilledTrack />\n            </SliderTrack>\n            <SliderThumb />\n          </Slider>\n        </ModalBody>\n\n        <ModalFooter bg=\"brandGray.dark\">\n          <Button onClick={onClose} fontSize=\"14px\" mr={6} variant=\"link\">\n            Cancel\n          </Button>\n          <Button\n            background=\"highlight.standard\"\n            color=\"white\"\n            type=\"submit\"\n            fontSize=\"14px\"\n            _hover={{ bg: 'highlight.hover' }}\n            _active={{ bg: 'highlight.active' }}\n            _focus={{ boxShadow: 'none' }}\n            onClick={showCroppedImage}\n          >\n            Apply\n          </Button>\n        </ModalFooter>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/DeleteMessageModal.tsx",
    "content": "import {\n  Avatar,\n  Box,\n  Button,\n  Flex,\n  LightMode,\n  Modal,\n  ModalBody,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n} from '@chakra-ui/react';\nimport React, { useState } from 'react';\nimport { deleteMessage } from '../../lib/api/handler/messages';\nimport { getTime } from '../../lib/utils/dateUtils';\nimport { Message } from '../../lib/models/message';\n\ninterface IProps {\n  message: Message;\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport const DeleteMessageModal: React.FC<IProps> = ({ message, isOpen, onClose }) => {\n  const [showError, toggleShow] = useState(false);\n\n  const handleDelete = async (): Promise<void> => {\n    try {\n      const { data } = await deleteMessage(message.id);\n      if (data) {\n        onClose();\n      }\n    } catch (err) {\n      toggleShow(true);\n    }\n  };\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n\n      <ModalContent bg=\"brandGray.light\">\n        <ModalHeader fontWeight=\"bold\" mb={0} pb={0}>\n          Delete Message\n        </ModalHeader>\n        <ModalBody>\n          <Text mb=\"4\">Are you sure you want to delete this message?</Text>\n\n          <Flex alignItems=\"center\" my=\"2\" mr=\"1\" justify=\"space-between\" boxShadow=\"dark-lg\" py={2}>\n            <Flex>\n              <Avatar h=\"40px\" w=\"40px\" ml=\"4\" mt=\"1\" src={message.user.image} />\n              <Box ml=\"3\">\n                <Flex alignItems=\"center\">\n                  <Text>{message.user.username}</Text>\n                  <Text fontSize=\"12px\" color=\"brandGray.accent\" ml=\"3\">\n                    {getTime(message.createdAt)}\n                  </Text>\n                </Flex>\n                <Text>{message.attachment?.filename ?? message.text}</Text>\n              </Box>\n            </Flex>\n          </Flex>\n\n          {showError && (\n            <Text my=\"2\" color=\"menuRed\" align=\"center\">\n              Server Error. Try again later\n            </Text>\n          )}\n        </ModalBody>\n\n        <ModalFooter bg=\"brandGray.dark\">\n          <Button onClick={onClose} mr={6} variant=\"link\" fontSize=\"14px\" _focus={{ outline: 'none' }}>\n            Cancel\n          </Button>\n          <LightMode>\n            <Button type=\"submit\" colorScheme=\"red\" fontSize=\"14px\" onClick={() => handleDelete()}>\n              Delete\n            </Button>\n          </LightMode>\n        </ModalFooter>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/EditMemberModal.tsx",
    "content": "import {\n  Button,\n  Divider,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n} from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React, { useState } from 'react';\nimport { useQuery } from '@tanstack/react-query';\nimport { ColorResult, TwitterPicker } from 'react-color';\nimport { InputField } from '../common/InputField';\nimport { toErrorMap } from '../../lib/utils/toErrorMap';\nimport { userStore } from '../../lib/stores/userStore';\nimport { MemberSchema } from '../../lib/utils/validation/member.schema';\nimport { changeGuildMemberSettings, getGuildMemberSettings } from '../../lib/api/handler/members';\n\ninterface IProps {\n  guildId: string;\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport const EditMemberModal: React.FC<IProps> = ({ guildId, isOpen, onClose }) => {\n  const current = userStore((state) => state.current);\n  const { data } = useQuery(['settings', guildId], () =>\n    getGuildMemberSettings(guildId).then((response) => response.data)\n  );\n  const [showError, toggleShow] = useState(false);\n\n  if (!data) return null;\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n      <ModalContent bg=\"brandGray.light\">\n        <Formik\n          initialValues={{\n            color: data.color,\n            nickname: data.nickname,\n          }}\n          validationSchema={MemberSchema}\n          onSubmit={async (values, { setErrors, setFieldValue }) => {\n            try {\n              // Default color --> Reset\n              if (values.color === '#fff') setFieldValue('color', null);\n\n              const { data: responseData } = await changeGuildMemberSettings(guildId, values);\n              if (responseData) {\n                onClose();\n              }\n            } catch (err: any) {\n              if (err?.response?.status === 500) {\n                toggleShow(true);\n              }\n              if (err?.response?.data?.errors) {\n                const errors = err?.response?.data?.errors;\n                setErrors(toErrorMap(errors));\n              }\n            }\n          }}\n        >\n          {({ isSubmitting, setFieldValue, values }) => (\n            <Form>\n              <ModalHeader fontWeight=\"bold\" pb={0}>\n                Change Appearance\n              </ModalHeader>\n              <ModalCloseButton _focus={{ outline: 'none' }} />\n              <ModalBody>\n                <InputField\n                  color={values.color ?? undefined}\n                  label=\"nickname\"\n                  name=\"nickname\"\n                  value={values.nickname ?? current?.username}\n                />\n                <Text\n                  mt=\"2\"\n                  ml=\"1\"\n                  color=\"brandGray.accent\"\n                  _hover={{\n                    cursor: 'pointer',\n                    color: 'highlight.standard',\n                  }}\n                  fontSize=\"14px\"\n                  onClick={() => setFieldValue('nickname', null)}\n                >\n                  Reset Nickname\n                </Text>\n\n                <Divider my=\"4\" />\n\n                <TwitterPicker\n                  color={values.color || '#fff'}\n                  onChangeComplete={(color: ColorResult) => {\n                    if (color) setFieldValue('color', color.hex);\n                  }}\n                  className=\"picker\"\n                />\n\n                <Text\n                  mt=\"2\"\n                  ml=\"1\"\n                  color=\"brandGray.accent\"\n                  _hover={{\n                    cursor: 'pointer',\n                    color: 'highlight.standard',\n                  }}\n                  fontSize=\"14px\"\n                  onClick={() => setFieldValue('color', '#fff')}\n                >\n                  Reset Color\n                </Text>\n\n                {showError && (\n                  <Text mt=\"4\" color=\"menuRed\" align=\"center\">\n                    Server Error. Try again later\n                  </Text>\n                )}\n              </ModalBody>\n\n              <ModalFooter bg=\"brandGray.dark\">\n                <Button onClick={onClose} mr={6} variant=\"link\" fontSize=\"14px\" _focus={{ outline: 'none' }}>\n                  Cancel\n                </Button>\n                <Button\n                  background=\"highlight.standard\"\n                  color=\"white\"\n                  type=\"submit\"\n                  _hover={{ bg: 'highlight.hover' }}\n                  _active={{ bg: 'highlight.active' }}\n                  _focus={{ boxShadow: 'none' }}\n                  isLoading={isSubmitting}\n                  fontSize=\"14px\"\n                >\n                  Save\n                </Button>\n              </ModalFooter>\n            </Form>\n          )}\n        </Formik>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/EditMessageModal.tsx",
    "content": "import {\n  Avatar,\n  Box,\n  Button,\n  Flex,\n  Input,\n  LightMode,\n  Modal,\n  ModalBody,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n} from '@chakra-ui/react';\nimport React, { useState } from 'react';\nimport { editMessage } from '../../lib/api/handler/messages';\nimport { getTime } from '../../lib/utils/dateUtils';\nimport { Message } from '../../lib/models/message';\n\ninterface IProps {\n  message: Message;\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport const EditMessageModal: React.FC<IProps> = ({ message, isOpen, onClose }) => {\n  const [text, setNewText] = useState(message.text!);\n  const [showError, toggleShow] = useState(false);\n\n  const handleSubmit = async (): Promise<void> => {\n    if (!text || !text.trim()) return;\n\n    try {\n      await editMessage(message.id, text.trim());\n      onClose();\n    } catch (err) {\n      toggleShow(true);\n    }\n  };\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n\n      <ModalContent bg=\"brandGray.light\">\n        <ModalHeader fontWeight=\"bold\" mb={0} pb={0}>\n          Edit Message\n        </ModalHeader>\n        <ModalBody>\n          <Flex alignItems=\"center\" my=\"2\" mr=\"1\" justify=\"space-between\" boxShadow=\"dark-lg\" py={2}>\n            <Flex alignItems=\"center\">\n              <Avatar h=\"40px\" w=\"40px\" ml=\"4\" src={message.user.image} />\n              <Box ml=\"3\">\n                <Flex alignItems=\"center\">\n                  <Text>{message.user.username}</Text>\n                  <Text fontSize=\"12px\" color=\"brandGray.accent\" ml=\"3\">\n                    {getTime(message.createdAt)}\n                  </Text>\n                </Flex>\n                <Input\n                  id=\"editMessage\"\n                  value={text}\n                  onChange={(e: any) => setNewText(e.target.value)}\n                  bg=\"brandGray.dark\"\n                  borderColor=\"black\"\n                  borderRadius=\"3px\"\n                  focusBorderColor=\"none\"\n                />\n              </Box>\n            </Flex>\n          </Flex>\n\n          {showError && (\n            <Text my=\"2\" color=\"menuRed\" align=\"center\">\n              Server Error. Try again later\n            </Text>\n          )}\n        </ModalBody>\n\n        <ModalFooter bg=\"brandGray.dark\">\n          <Button onClick={onClose} mr={6} variant=\"link\" fontSize=\"14px\" _focus={{ outline: 'none' }}>\n            Cancel\n          </Button>\n          <LightMode>\n            <Button colorScheme=\"green\" fontSize=\"14px\" onClick={handleSubmit}>\n              Save\n            </Button>\n          </LightMode>\n        </ModalFooter>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/GuildSettingsModal.tsx",
    "content": "import {\n  Avatar,\n  Box,\n  Button,\n  Divider,\n  Flex,\n  IconButton,\n  LightMode,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n  Tooltip,\n  useDisclosure,\n} from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React, { useRef, useState } from 'react';\nimport { FaRegTrashAlt } from 'react-icons/fa';\nimport { IoCheckmarkCircle, IoPersonRemove } from 'react-icons/io5';\nimport { ImHammer2 } from 'react-icons/im';\nimport { BiUnlink } from 'react-icons/bi';\nimport { useQuery, useQueryClient } from '@tanstack/react-query';\nimport { InputField } from '../common/InputField';\nimport { toErrorMap } from '../../lib/utils/toErrorMap';\nimport { useGetCurrentGuild } from '../../lib/utils/hooks/useGetCurrentGuild';\nimport { GuildSchema } from '../../lib/utils/validation/guild.schema';\nimport { deleteGuild, editGuild, invalidateInviteLinks } from '../../lib/api/handler/guilds';\nimport { CropImageModal } from './CropImageModal';\nimport { channelScrollbarCss } from '../layouts/guild/css/ChannelScrollerCSS';\nimport { getBanList, unbanMember } from '../../lib/api/handler/members';\nimport { Member } from '../../lib/models/member';\n\ninterface IProps {\n  guildId: string;\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nenum SettingsScreen {\n  START,\n  CONFIRM,\n  BANLIST,\n}\n\nexport const GuildSettingsModal: React.FC<IProps> = ({ guildId, isOpen, onClose }) => {\n  const guild = useGetCurrentGuild(guildId);\n\n  const [screen, setScreen] = useState(SettingsScreen.START);\n  const [isReset, setIsReset] = useState(false);\n  const [showError, toggleShow] = useState(false);\n\n  const goBack = (): void => setScreen(SettingsScreen.START);\n  const submitClose = (): void => {\n    setScreen(SettingsScreen.START);\n    onClose();\n  };\n\n  const { isOpen: cropperIsOpen, onOpen: cropperOnOpen, onClose: cropperOnClose } = useDisclosure();\n\n  const inputFile: any = useRef(null);\n  const [imageUrl, setImageUrl] = useState<string | null>(guild?.icon || '');\n  const [cropImage, setCropImage] = useState('');\n  const [croppedImage, setCroppedImage] = useState<any>(null);\n\n  const applyCrop = (file: Blob): void => {\n    setImageUrl(URL.createObjectURL(file));\n    setCroppedImage(new File([file], 'icon', { type: 'image/jpeg' }));\n    cropperOnClose();\n  };\n\n  if (!guild) return null;\n\n  const invalidateInvites = async (): Promise<void> => {\n    try {\n      const { data } = await invalidateInviteLinks(guild!.id);\n      if (data) {\n        setIsReset(true);\n      }\n    } catch (err) {}\n  };\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n      {screen === SettingsScreen.START && (\n        <ModalContent bg=\"brandGray.light\">\n          <Formik\n            initialValues={{\n              name: guild.name,\n            }}\n            validationSchema={GuildSchema}\n            onSubmit={async (values, { setErrors, resetForm }) => {\n              try {\n                const formData = new FormData();\n                formData.append('name', values.name);\n                if (cropImage) {\n                  formData.append('image', croppedImage);\n                } else if (imageUrl) {\n                  formData.append('icon', imageUrl);\n                }\n\n                const { data } = await editGuild(guildId, formData);\n                if (data) {\n                  resetForm();\n                  onClose();\n                }\n              } catch (err: any) {\n                if (err?.response?.status === 500) {\n                  toggleShow(true);\n                }\n                if (err?.response?.data?.errors) {\n                  const errors = err?.response?.data?.errors;\n                  setErrors(toErrorMap(errors));\n                }\n              }\n            }}\n          >\n            {({ isSubmitting }) => (\n              <Form>\n                <ModalHeader textAlign=\"center\" fontWeight=\"bold\" pb={0}>\n                  Server Overview\n                </ModalHeader>\n                <ModalCloseButton _focus={{ outline: 'none' }} />\n                <ModalBody>\n                  <Flex mb=\"4\" justify=\"center\">\n                    <Box textAlign=\"center\">\n                      <Tooltip label=\"Change Icon\" aria-label=\"Change Icon\">\n                        <Avatar\n                          size=\"xl\"\n                          name={guild?.name[0]}\n                          bg=\"brandGray.darker\"\n                          color=\"#fff\"\n                          src={imageUrl || ''}\n                          _hover={{\n                            cursor: 'pointer',\n                            opacity: 0.5,\n                          }}\n                          onClick={() => inputFile.current.click()}\n                        />\n                      </Tooltip>\n                      <Text\n                        mt=\"2\"\n                        _hover={{\n                          cursor: 'pointer',\n                          color: 'brandGray.accent',\n                        }}\n                        onClick={() => {\n                          setCroppedImage(null);\n                          setImageUrl(null);\n                        }}\n                      >\n                        Remove\n                      </Text>\n                    </Box>\n                    <input\n                      type=\"file\"\n                      name=\"image\"\n                      accept=\"image/*\"\n                      ref={inputFile}\n                      hidden\n                      onChange={async (e) => {\n                        if (!e.currentTarget.files) return;\n                        setCropImage(URL.createObjectURL(e.currentTarget.files[0]));\n                        cropperOnOpen();\n                      }}\n                    />\n                  </Flex>\n\n                  <InputField label=\"server name\" name=\"name\" />\n\n                  <Divider my=\"4\" />\n\n                  <Text fontWeight=\"semibold\" mb={2}>\n                    Additional Configuration\n                  </Text>\n\n                  <Flex align=\"center\" justify=\"space-between\" mb=\"2\">\n                    <Button\n                      onClick={invalidateInvites}\n                      fontSize=\"14px\"\n                      rightIcon={isReset ? <IoCheckmarkCircle /> : <BiUnlink />}\n                      colorScheme={isReset ? 'green' : 'gray'}\n                    >\n                      Invalidate Links\n                    </Button>\n                    <Button onClick={() => setScreen(SettingsScreen.BANLIST)} fontSize=\"14px\" rightIcon={<ImHammer2 />}>\n                      Bans\n                    </Button>\n                  </Flex>\n                  <Flex align=\"center\" justify=\"space-between\" mb=\"2\">\n                    <LightMode>\n                      <Button\n                        onClick={() => setScreen(SettingsScreen.CONFIRM)}\n                        colorScheme=\"red\"\n                        variant=\"ghost\"\n                        fontSize=\"14px\"\n                        textColor=\"menuRed\"\n                        rightIcon={<FaRegTrashAlt />}\n                      >\n                        Delete Server\n                      </Button>\n                    </LightMode>\n                  </Flex>\n                  {showError && (\n                    <Text my=\"2\" color=\"menuRed\" align=\"center\">\n                      Server Error. Try again later\n                    </Text>\n                  )}\n                </ModalBody>\n\n                <ModalFooter bg=\"brandGray.dark\">\n                  <Button onClick={onClose} mr={6} variant=\"link\" fontSize=\"14px\">\n                    Cancel\n                  </Button>\n                  <Button\n                    background=\"highlight.standard\"\n                    color=\"white\"\n                    type=\"submit\"\n                    _hover={{ bg: 'highlight.hover' }}\n                    _active={{ bg: 'highlight.active' }}\n                    _focus={{ boxShadow: 'none' }}\n                    isLoading={isSubmitting}\n                    fontSize=\"14px\"\n                  >\n                    Save Changes\n                  </Button>\n                </ModalFooter>\n              </Form>\n            )}\n          </Formik>\n          {cropperIsOpen && (\n            <CropImageModal\n              isOpen={cropperIsOpen}\n              onClose={cropperOnClose}\n              initialImage={cropImage}\n              applyCrop={applyCrop}\n            />\n          )}\n        </ModalContent>\n      )}\n      {screen === SettingsScreen.CONFIRM && (\n        <DeleteGuildModal goBack={goBack} submitClose={submitClose} name={guild.name} guildId={guildId} />\n      )}\n      {screen === SettingsScreen.BANLIST && <BanListModal goBack={goBack} guildId={guildId} />}\n    </Modal>\n  );\n};\n\ninterface IScreenProps {\n  goBack: () => void;\n  submitClose: () => void;\n  name: string;\n  guildId: string;\n}\n\nconst DeleteGuildModal: React.FC<IScreenProps> = ({ goBack, submitClose, name, guildId }) => {\n  const [showError, toggleShow] = useState(false);\n\n  const handleDelete = async (): Promise<void> => {\n    try {\n      const { data } = await deleteGuild(guildId);\n      if (data) {\n        submitClose();\n      }\n    } catch (err: any) {\n      if (err?.response?.status === 500) {\n        toggleShow(true);\n      }\n    }\n  };\n\n  return (\n    <ModalContent bg=\"brandGray.light\">\n      <ModalHeader fontWeight=\"bold\" pb=\"0\">\n        Delete {name}\n      </ModalHeader>\n      <ModalBody pb={3}>\n        <Text>\n          Are you sure you want to delete <b>{name}</b>? This cannot be undone.\n        </Text>\n\n        {showError && (\n          <Text my=\"2\" color=\"menuRed\" align=\"center\">\n            Server Error. Try again later\n          </Text>\n        )}\n      </ModalBody>\n\n      <ModalFooter bg=\"brandGray.dark\">\n        <Button mr={6} variant=\"link\" onClick={goBack} fontSize=\"14px\" _focus={{ outline: 'none' }}>\n          Cancel\n        </Button>\n        <LightMode>\n          <Button colorScheme=\"red\" fontSize=\"14px\" onClick={() => handleDelete()}>\n            Delete Server\n          </Button>\n        </LightMode>\n      </ModalFooter>\n    </ModalContent>\n  );\n};\n\ninterface IBanScreenProps {\n  goBack: () => void;\n  guildId: string;\n}\n\nconst BanListModal: React.FC<IBanScreenProps> = ({ goBack, guildId }) => {\n  const key = ['bans', guildId];\n  const { data } = useQuery(key, () => getBanList(guildId).then((response) => response.data));\n  const cache = useQueryClient();\n\n  const unbanUser = async (id: string): Promise<void> => {\n    try {\n      const { data: responseData } = await unbanMember(guildId, id);\n      if (responseData) {\n        cache.setQueryData<Member[]>(key, (d) => d?.filter((b) => b.id !== id) ?? []);\n      }\n    } catch (err) {}\n  };\n\n  return (\n    <ModalContent bg=\"brandGray.light\" maxH=\"500px\">\n      <ModalHeader fontWeight=\"bold\" pb=\"0\">\n        {data?.length} Bans\n      </ModalHeader>\n      <ModalBody pb={3} overflowY=\"auto\" css={channelScrollbarCss}>\n        <Text mb={2}>Bans are by account. Click on the icon to unban.</Text>\n\n        {data?.map((m) => (\n          <Flex\n            p=\"3\"\n            _hover={{\n              bg: 'brandGray.dark',\n              borderRadius: '5px',\n            }}\n            align=\"center\"\n            justify=\"space-between\"\n          >\n            <Flex align=\"center\" w=\"full\">\n              <Avatar size=\"sm\" src={m.image} />\n              <Text ml=\"2\">{m.username}</Text>\n            </Flex>\n            <IconButton\n              icon={<IoPersonRemove />}\n              borderRadius=\"50%\"\n              aria-label=\"unban user\"\n              onClick={async (e) => {\n                e.preventDefault();\n                await unbanUser(m.id);\n              }}\n            />\n          </Flex>\n        ))}\n        {data?.length === 0 && <Text>No bans yet.</Text>}\n      </ModalBody>\n\n      <ModalFooter bg=\"brandGray.dark\">\n        <Button mr={6} variant=\"link\" onClick={goBack} fontSize=\"14px\" _focus={{ outline: 'none' }}>\n          Back\n        </Button>\n      </ModalFooter>\n    </ModalContent>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/InviteModal.tsx",
    "content": "import {\n  Button,\n  Checkbox,\n  Input,\n  InputGroup,\n  InputRightElement,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n  useClipboard,\n} from '@chakra-ui/react';\nimport React, { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { getInviteLink } from '../../lib/api/handler/guilds';\nimport { RouterProps } from '../../lib/models/routerProps';\n\ninterface InviteModalProps {\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport const InviteModal: React.FC<InviteModalProps> = ({ isOpen, onClose }) => {\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n  const [inviteLink, setInviteLink] = useState('');\n  const { hasCopied, onCopy } = useClipboard(inviteLink);\n  const [isPermanent, setPermanent] = useState(false);\n\n  useEffect(() => {\n    if (isOpen) {\n      const fetchLink = async (): Promise<void> => {\n        try {\n          const { data } = await getInviteLink(guildId, isPermanent);\n          if (data) setInviteLink(data);\n        } catch (err) {}\n      };\n      fetchLink();\n    }\n  }, [isOpen, setInviteLink, guildId, isPermanent]);\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n      <ModalContent bg=\"brandGray.light\">\n        <ModalHeader textAlign=\"center\" fontWeight=\"bold\" pb=\"0\">\n          Invite Link\n        </ModalHeader>\n        <ModalCloseButton _focus={{ outline: 'none' }} />\n        <ModalBody>\n          <Text mb=\"4\">Share this link with others to grant access to this server</Text>\n\n          <Checkbox onChange={(e) => setPermanent(e.target.checked)} mb={4}>\n            Make it unlimited / Never reset\n          </Checkbox>\n\n          <InputGroup>\n            <Input\n              id=\"invite-link\"\n              bg=\"brandGray.dark\"\n              borderColor={hasCopied ? 'brandGreen' : 'black'}\n              borderRadius=\"3px\"\n              focusBorderColor=\"highlight.standard\"\n              value={inviteLink}\n              isReadOnly\n            />\n            <InputRightElement width=\"4.5rem\">\n              <Button\n                h=\"1.75rem\"\n                size=\"sm\"\n                bg={hasCopied ? 'brandGreen' : 'highlight.standard'}\n                color=\"white\"\n                type=\"submit\"\n                fontSize=\"14px\"\n                _hover={{ bg: 'highlight.hover' }}\n                _active={{ bg: 'highlight.active' }}\n                _focus={{ boxShadow: 'none' }}\n                onClick={onCopy}\n              >\n                {hasCopied ? 'Copied' : 'Copy'}\n              </Button>\n            </InputRightElement>\n          </InputGroup>\n\n          <Text my=\"2\" fontSize=\"12px\">\n            {isPermanent\n              ? \"Your invite link won't expire\"\n              : 'Your invite link expires in 1 day and can only be used once'}\n          </Text>\n        </ModalBody>\n\n        <ModalFooter bg=\"brandGray.dark\" />\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/ModActionModal.tsx",
    "content": "import {\n  Button,\n  LightMode,\n  Modal,\n  ModalBody,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n} from '@chakra-ui/react';\nimport React from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useParams } from 'react-router-dom';\nimport { mKey } from '../../lib/utils/querykeys';\nimport { Member } from '../../lib/models/member';\nimport { RouterProps } from '../../lib/models/routerProps';\nimport { banMember, kickMember } from '../../lib/api/handler/members';\n\ninterface IProps {\n  member: Member;\n  isOpen: boolean;\n  isBan: boolean;\n  onClose: () => void;\n}\n\nexport const ModActionModal: React.FC<IProps> = ({ member, isOpen, onClose, isBan }) => {\n  const cache = useQueryClient();\n  const action = isBan ? 'Ban ' : 'Kick ';\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n\n      <ModalContent bg=\"brandGray.light\">\n        <ModalHeader textTransform=\"uppercase\" fontWeight=\"bold\" fontSize=\"14px\" mb={0} pb={0}>\n          {action}&apos;{member.username}&apos;\n        </ModalHeader>\n        <ModalBody>\n          <Text mb=\"4\">\n            Are you sure you want to {action.toLocaleLowerCase()} @{member.username}?\n            {!isBan && ' They will be able to rejoin again with a new invite.'}\n          </Text>\n        </ModalBody>\n\n        <ModalFooter bg=\"brandGray.dark\">\n          <Button onClick={onClose} mr={6} variant=\"link\" fontSize=\"14px\" _focus={{ outline: 'none' }}>\n            Cancel\n          </Button>\n          <LightMode>\n            <Button\n              colorScheme=\"red\"\n              fontSize=\"14px\"\n              onClick={async () => {\n                onClose();\n                try {\n                  const { data } = isBan ? await banMember(guildId, member.id) : await kickMember(guildId, member.id);\n                  if (data) {\n                    cache.setQueryData<Member[]>([mKey, guildId], (d) => d!.filter((f) => f.id !== member.id) ?? []);\n                  }\n                } catch (err) {}\n              }}\n            >\n              {action}\n            </Button>\n          </LightMode>\n        </ModalFooter>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/modals/RemoveFriendModal.tsx",
    "content": "import {\n  Button,\n  LightMode,\n  Modal,\n  ModalBody,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverlay,\n  Text,\n} from '@chakra-ui/react';\nimport React from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { removeFriend } from '../../lib/api/handler/account';\nimport { fKey } from '../../lib/utils/querykeys';\nimport { Friend } from '../../lib/models/friend';\n\ninterface IProps {\n  member: Friend;\n  isOpen: boolean;\n  onClose: () => void;\n}\n\nexport const RemoveFriendModal: React.FC<IProps> = ({ member, isOpen, onClose }) => {\n  const cache = useQueryClient();\n\n  return (\n    <Modal isOpen={isOpen} onClose={onClose} isCentered>\n      <ModalOverlay />\n\n      <ModalContent bg=\"brandGray.light\">\n        <ModalHeader textTransform=\"uppercase\" fontWeight=\"bold\" mb={0} pb={0}>\n          Remove &apos;{member?.username}&apos;\n        </ModalHeader>\n        <ModalBody>\n          <Text mb=\"4\">\n            Are you sure you want to permanently remove <b>{member?.username}</b> from your friends?\n          </Text>\n        </ModalBody>\n\n        <ModalFooter bg=\"brandGray.dark\">\n          <Button onClick={onClose} mr={6} variant=\"link\" fontSize=\"14px\" _focus={{ outline: 'none' }}>\n            Cancel\n          </Button>\n          <LightMode>\n            <Button\n              colorScheme=\"red\"\n              fontSize=\"14px\"\n              onClick={async () => {\n                onClose();\n                try {\n                  const { data } = await removeFriend(member.id);\n                  if (data) {\n                    cache.setQueryData<Friend[]>([fKey], (d) => d?.filter((f) => f.id !== member.id) ?? []);\n                  }\n                } catch (err) {}\n              }}\n            >\n              Remove Friend\n            </Button>\n          </LightMode>\n        </ModalFooter>\n      </ModalContent>\n    </Modal>\n  );\n};\n"
  },
  {
    "path": "web/src/components/sections/AddGuildIcon.tsx",
    "content": "import React, { useState } from 'react';\nimport { Flex } from '@chakra-ui/react';\nimport { AiOutlinePlus } from 'react-icons/ai';\nimport { StyledTooltip } from './StyledTooltip';\nimport { HoverGuildPill } from '../common/GuildPills';\n\ninterface AddGuildIconProps {\n  onOpen: () => void;\n}\n\nexport const AddGuildIcon: React.FC<AddGuildIconProps> = ({ onOpen }) => {\n  const [isHover, setHover] = useState(false);\n\n  return (\n    <>\n      {isHover && <HoverGuildPill />}\n      <StyledTooltip label=\"Add a Server\" position=\"right\">\n        <Flex\n          id=\"add-guild-icon\"\n          direction=\"column\"\n          m=\"auto\"\n          align=\"center\"\n          justify=\"center\"\n          bg=\"brandGray.light\"\n          borderRadius=\"50%\"\n          h=\"48px\"\n          w=\"48px\"\n          _hover={{\n            cursor: 'pointer',\n            borderRadius: '35%',\n            bg: 'brandGreen',\n            color: 'white',\n          }}\n          onClick={onOpen}\n          onMouseLeave={() => setHover(false)}\n          onMouseEnter={() => setHover(true)}\n        >\n          <AiOutlinePlus fontSize=\"25px\" />\n        </Flex>\n      </StyledTooltip>\n    </>\n  );\n};\n"
  },
  {
    "path": "web/src/components/sections/DMPlaceholder.tsx",
    "content": "import React from 'react';\nimport { Box, Flex } from '@chakra-ui/react';\n\nexport const DMPlaceholder: React.FC = () => (\n  <Flex align=\"center\" m=\"3\">\n    <Box w=\"32px\" h=\"32px\" borderRadius=\"50%\" bg=\"brandGray.light\" />\n    <Box ml={2} height=\"20px\" w=\"144px\" bg=\"brandGray.light\" borderRadius=\"10px\" />\n  </Flex>\n);\n"
  },
  {
    "path": "web/src/components/sections/DateDivider.tsx",
    "content": "import { Divider, Flex, Text } from '@chakra-ui/react';\nimport React from 'react';\nimport { formatDivider } from '../../lib/utils/dateUtils';\n\ninterface DateDividerProps {\n  date: string;\n}\n\nexport const DateDivider: React.FC<DateDividerProps> = ({ date }) => (\n  <Flex textAlign=\"center\" align=\"center\" mt=\"2\" mx=\"4\" key={date}>\n    <Divider />\n    <Text w={['75%', '75%', '75%', '40%', '25%']} fontSize=\"12px\" color=\"brandGray.accent\">\n      {formatDivider(date)}\n    </Text>\n    <Divider />\n  </Flex>\n);\n"
  },
  {
    "path": "web/src/components/sections/Footer.tsx",
    "content": "import React from 'react';\nimport { Box, Flex, Link, Stack, Text } from '@chakra-ui/react';\nimport { AiOutlineApi, AiOutlineGithub } from 'react-icons/ai';\nimport { SiSocketdotio } from 'react-icons/si';\nimport { IconType } from 'react-icons';\nimport { StyledTooltip } from './StyledTooltip';\n\ntype FooterLinkProps = {\n  icon?: IconType;\n  href?: string;\n  label?: string;\n};\n\nconst FooterLink: React.FC<FooterLinkProps> = ({ icon, href, label }) => (\n  <StyledTooltip label={label!} position=\"top\">\n    <Link display=\"inline-block\" href={href} aria-label={label} isExternal mx={2}>\n      <Box as={icon} width=\"24px\" height=\"24px\" color=\"gray.400\" _hover={{ color: 'gray.100' }} />\n    </Link>\n  </StyledTooltip>\n);\n\nconst links = [\n  {\n    icon: AiOutlineGithub,\n    label: 'GitHub',\n    href: 'https://github.com/sentrionic/Valkyrie',\n  },\n  {\n    icon: AiOutlineApi,\n    label: 'REST API',\n    href: `${process.env.REACT_APP_API!}/swagger/index.html`,\n  },\n  {\n    icon: SiSocketdotio,\n    label: 'Websocket',\n    href: `${process.env.REACT_APP_API!}`,\n  },\n];\n\nexport const Footer: React.FC = () => (\n  <Flex bottom={0} as=\"footer\" align=\"center\" justify=\"center\" w=\"100%\" p={8}>\n    <Box textAlign=\"center\">\n      <Text fontSize=\"xl\">\n        <span>Valkyrie | 2022</span>\n      </Text>\n      <Text>This is not a real commercial app.</Text>\n      <Stack mt={2} isInline justify=\"center\" spacing=\"6\">\n        {links.map((link) => (\n          // eslint-disable-next-line react/jsx-props-no-spreading\n          <FooterLink key={link.href} {...link} />\n        ))}\n      </Stack>\n    </Box>\n  </Flex>\n);\n"
  },
  {
    "path": "web/src/components/sections/FriendsListButton.tsx",
    "content": "import React from 'react';\nimport { Flex, Icon, Text } from '@chakra-ui/react';\nimport { FiUsers } from 'react-icons/fi';\nimport { Link, useLocation } from 'react-router-dom';\nimport { PingIcon } from '../common/NotificationIcon';\nimport { homeStore } from '../../lib/stores/homeStore';\n\nexport const FriendsListButton: React.FC = () => {\n  const currentPath = '/channels/me';\n  const location = useLocation();\n  const isActive = location.pathname === currentPath;\n  const requests = homeStore((state) => state.requestCount);\n\n  return (\n    <Link to=\"/channels/me\">\n      <Flex\n        m=\"2\"\n        p=\"3\"\n        align=\"center\"\n        justify=\"space-between\"\n        color={isActive ? '#fff' : 'brandGray.accent'}\n        _hover={{\n          bg: 'brandGray.light',\n          borderRadius: '5px',\n          cursor: 'pointer',\n          color: '#fff',\n        }}\n        bg={isActive ? 'brandGray.active' : undefined}\n      >\n        <Flex align=\"center\">\n          <Icon as={FiUsers} fontSize=\"20px\" />\n          <Text fontSize=\"14px\" ml=\"4\" fontWeight=\"semibold\">\n            Friends\n          </Text>\n        </Flex>\n        {requests > 0 && <PingIcon count={requests} />}\n      </Flex>\n    </Link>\n  );\n};\n"
  },
  {
    "path": "web/src/components/sections/GlobalState.tsx",
    "content": "import React, { useEffect } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { userStore } from '../../lib/stores/userStore';\nimport { getSocket } from '../../lib/api/getSocket';\nimport { homeStore } from '../../lib/stores/homeStore';\nimport { nKey } from '../../lib/utils/querykeys';\nimport { DMChannel, DMNotification } from '../../lib/models/dm';\n\ntype WSMessage = { action: 'new_dm_notification'; data: DMChannel } | { action: 'send_request' };\n\ninterface IProps {\n  children: React.ReactNode;\n}\n\nexport const GlobalState: React.FC<IProps> = ({ children }) => {\n  const current = userStore((state) => state.current);\n  const inc = homeStore((state) => state.increment);\n  const cache = useQueryClient();\n\n  // eslint-disable-next-line consistent-return\n  useEffect(() => {\n    if (current) {\n      const disconnect = (): void => {\n        socket.send(JSON.stringify({ action: 'toggleOffline' }));\n        socket.close();\n      };\n\n      const socket = getSocket();\n      socket.send(JSON.stringify({ action: 'toggleOnline' }));\n      socket.send(\n        JSON.stringify({\n          action: 'joinUser',\n          room: current?.id,\n        })\n      );\n\n      socket.addEventListener('message', (event) => {\n        const response: WSMessage = JSON.parse(event.data);\n        switch (response.action) {\n          case 'new_dm_notification': {\n            const channel = response.data;\n            if (channel.user.id !== current.id) {\n              cache.setQueryData<DMNotification[]>([nKey], (data) => {\n                if (!data) return [{ ...channel, count: 1 }];\n\n                const index = data.findIndex((c) => c.id === channel.id);\n\n                // DM exists, increment message count\n                if (index !== -1 && index !== undefined) {\n                  return [\n                    {\n                      ...channel,\n                      count: data[index].count + 1,\n                    },\n                    ...data.filter((c) => c.id !== channel.id),\n                  ];\n                }\n                // Add the new DM to the list\n                return [\n                  {\n                    ...channel,\n                    count: 1,\n                  },\n                  ...data,\n                ];\n              });\n            }\n            break;\n          }\n\n          case 'send_request': {\n            if (!window.location.pathname.includes('/channels/me')) {\n              inc();\n            }\n            break;\n          }\n\n          default:\n            break;\n        }\n      });\n\n      window.addEventListener('beforeunload', disconnect);\n\n      return () => disconnect();\n    }\n  }, [current, inc, cache]);\n\n  /* eslint-disable-next-line react/jsx-no-useless-fragment */\n  return <>{children}</>;\n};\n"
  },
  {
    "path": "web/src/components/sections/Hero.tsx",
    "content": "/* eslint-disable react/jsx-props-no-spreading */\nimport React from 'react';\nimport { Link } from 'react-router-dom';\nimport { Box, Button, Flex, Heading, Image, Link as CLink, Stack, Text } from '@chakra-ui/react';\n\ninterface HeroProps {\n  title: string;\n  subtitle: string;\n  image: string;\n  ctaLink: string;\n  ctaText: string;\n}\n\nexport const Hero: React.FC<HeroProps> = ({ title, subtitle, image, ctaLink, ctaText, ...rest }) => (\n  <Flex\n    align=\"center\"\n    justify={{\n      base: 'center',\n      md: 'space-around',\n      xl: 'space-between',\n    }}\n    direction={{\n      base: 'column-reverse',\n      md: 'row',\n    }}\n    wrap=\"nowrap\"\n    minH=\"70vh\"\n    px={8}\n    mb={16}\n    {...rest}\n  >\n    <Stack\n      spacing={4}\n      w={{\n        base: '80%',\n        md: '40%',\n      }}\n      align={['center', 'center', 'flex-start', 'flex-start']}\n    >\n      <Heading as=\"h1\" size=\"xl\" fontWeight=\"bold\" textAlign={['center', 'center', 'left', 'left']}>\n        {title}\n      </Heading>\n      <Heading\n        as=\"h2\"\n        size=\"md\"\n        opacity=\"0.8\"\n        fontWeight=\"normal\"\n        lineHeight={1.5}\n        textAlign={['center', 'center', 'left', 'left']}\n      >\n        {subtitle}\n      </Heading>\n      <Link to={ctaLink}>\n        <Button colorScheme=\"blue\" borderRadius=\"8px\" py=\"4\" px=\"4\" lineHeight=\"1\" size=\"md\">\n          {ctaText}\n        </Button>\n      </Link>\n      <Text fontSize=\"xs\" mt={2} textAlign=\"center\" opacity=\"0.6\">\n        Got an account already?{' '}\n        <CLink as={Link} to=\"/login\" ml=\"1\" color=\"highlight.standard\" _focus={{ outline: 'none' }}>\n          Sign in\n        </CLink>\n      </Text>\n    </Stack>\n    <Box\n      w={{\n        base: '80%',\n        sm: '60%',\n        md: '50%',\n      }}\n      mb={{\n        base: 12,\n        md: 0,\n      }}\n    >\n      <Image src={image} boxSize=\"50%\" rounded=\"1rem\" />\n    </Box>\n  </Flex>\n);\n"
  },
  {
    "path": "web/src/components/sections/HomeIcon.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Flex, useColorModeValue } from '@chakra-ui/react';\nimport { Link, useLocation } from 'react-router-dom';\nimport { StyledTooltip } from './StyledTooltip';\nimport { ActiveGuildPill, HoverGuildPill } from '../common/GuildPills';\nimport { homeStore } from '../../lib/stores/homeStore';\nimport { NotificationIcon } from '../common/NotificationIcon';\n\nexport const HomeIcon: React.FC = () => {\n  const location = useLocation();\n  const isActive = location.pathname === '/channels/me';\n  const [isHover, setHover] = useState(false);\n\n  const notification = homeStore((state) => state.notifCount);\n  const reset = homeStore((state) => state.reset);\n\n  useEffect(() => {\n    if (isActive) reset();\n  });\n\n  return (\n    <StyledTooltip label=\"Home\" position=\"right\">\n      <Flex direction=\"column\" my=\"2\" align=\"center\">\n        {isActive && <ActiveGuildPill />}\n        {isHover && <HoverGuildPill />}\n        <Link to=\"/channels/me\">\n          <Flex\n            direction=\"column\"\n            m=\"auto\"\n            align=\"center\"\n            justify=\"center\"\n            bg={isActive ? 'highlight.standard' : 'brandGray.light'}\n            borderRadius={isActive ? '35%' : '50%'}\n            h=\"48px\"\n            w=\"48px\"\n            color=\"white\"\n            position=\"relative\"\n            _hover={{\n              cursor: 'pointer',\n              borderRadius: '35%',\n              bg: 'highlight.standard',\n            }}\n            onMouseLeave={() => setHover(false)}\n            onMouseEnter={() => setHover(true)}\n          >\n            <Logo />\n            {notification > 0 && <NotificationIcon count={notification} />}\n          </Flex>\n        </Link>\n      </Flex>\n    </StyledTooltip>\n  );\n};\n\nconst Logo: React.FC = () => {\n  const fill = useColorModeValue('#2D3748', '#fff');\n  return (\n    <svg width=\"27pt\" height=\"22pt\" viewBox=\"0 0 27 22\" version=\"1.1\">\n      <g id=\"surface1\" fill={fill}>\n        {/* eslint-disable-next-line max-len */}\n        <path d=\"M 0.0625 0.125 C 0.0625 0.152344 1.109375 2.65625 1.683594 4 C 1.757812 4.167969 1.808594 4.316406 1.804688 4.332031 C 1.796875 4.359375 1.523438 4.140625 1.105469 3.777344 C 1.074219 3.75 1.042969 3.734375 1.035156 3.746094 C 1.023438 3.753906 1.375 4.570312 1.8125 5.558594 C 2.25 6.550781 2.601562 7.363281 2.59375 7.367188 C 2.59375 7.375 2.476562 7.292969 2.339844 7.179688 C 2.207031 7.070312 2.089844 6.988281 2.082031 6.996094 C 2.066406 7.011719 2.871094 8.894531 3.070312 9.3125 C 3.125 9.4375 3.207031 9.617188 3.242188 9.710938 C 3.332031 9.945312 3.65625 10.625 3.726562 10.734375 C 3.761719 10.777344 3.785156 10.859375 3.785156 10.914062 C 3.785156 11.035156 3.851562 11.125 4.035156 11.253906 C 4.15625 11.339844 5.554688 12.398438 5.59375 12.433594 C 5.597656 12.441406 5.316406 12.996094 4.964844 13.671875 C 4.527344 14.519531 4.34375 14.910156 4.367188 14.921875 C 4.390625 14.9375 4.601562 15.019531 4.835938 15.109375 C 6.234375 15.636719 7.089844 16.0625 8.023438 16.714844 C 9.449219 17.707031 11.117188 19.269531 12.945312 21.316406 C 13.269531 21.679688 13.542969 21.972656 13.554688 21.96875 C 13.5625 21.960938 13.828125 21.671875 14.140625 21.324219 C 14.894531 20.480469 16.570312 18.777344 17.15625 18.25 C 19.023438 16.585938 20.238281 15.847656 22.375 15.0625 C 22.566406 14.996094 22.726562 14.921875 22.730469 14.902344 C 22.742188 14.882812 22.46875 14.328125 22.125 13.667969 C 21.785156 13.003906 21.503906 12.457031 21.503906 12.445312 C 21.503906 12.433594 22.484375 11.675781 23.101562 11.21875 C 23.214844 11.132812 23.285156 11.058594 23.285156 11.019531 C 23.285156 10.941406 23.402344 10.679688 23.558594 10.417969 C 23.621094 10.3125 23.71875 10.097656 23.769531 9.949219 C 23.820312 9.800781 23.910156 9.566406 23.976562 9.429688 C 24.070312 9.210938 24.816406 7.5 24.972656 7.125 C 25.003906 7.050781 25.019531 6.988281 25.003906 6.988281 C 24.992188 6.988281 24.875 7.078125 24.746094 7.183594 C 24.617188 7.292969 24.507812 7.371094 24.5 7.367188 C 24.488281 7.351562 25.542969 4.949219 25.960938 4.035156 C 26.027344 3.882812 26.082031 3.757812 26.082031 3.75 C 26.082031 3.707031 25.988281 3.773438 25.65625 4.058594 C 25.445312 4.238281 25.269531 4.378906 25.261719 4.375 C 25.261719 4.367188 25.28125 4.300781 25.316406 4.222656 C 26.042969 2.519531 27.007812 0.207031 27.007812 0.175781 C 27.007812 0.148438 27 0.128906 26.992188 0.128906 C 26.953125 0.128906 24.371094 2.328125 24.25 2.460938 C 24.128906 2.597656 23.972656 2.722656 23.492188 3.0625 C 23.386719 3.132812 23.230469 3.265625 23.140625 3.351562 C 23.054688 3.4375 21.894531 4.425781 20.566406 5.546875 C 19.234375 6.664062 18.128906 7.601562 18.105469 7.621094 C 18.066406 7.65625 18.109375 7.734375 18.332031 8.066406 C 18.992188 9.050781 19.191406 9.648438 19.195312 10.628906 C 19.195312 11.160156 19.164062 11.371094 19.03125 11.847656 C 18.949219 12.148438 18.671875 12.886719 18.652344 12.863281 C 18.648438 12.859375 18.707031 12.519531 18.785156 12.105469 C 18.972656 11.132812 18.972656 10.941406 18.769531 10.511719 C 18.515625 9.964844 18.011719 9.269531 17.441406 8.675781 L 17.109375 8.332031 L 17.355469 8.046875 C 17.488281 7.890625 17.589844 7.757812 17.585938 7.757812 C 17.582031 7.75 17.433594 7.65625 17.257812 7.542969 C 16.507812 7.0625 15.875 6.535156 15.234375 5.859375 C 14.640625 5.234375 14.246094 4.675781 13.664062 3.632812 C 13.570312 3.457031 13.539062 3.445312 13.476562 3.550781 C 13.453125 3.59375 13.300781 3.855469 13.132812 4.136719 C 12.703125 4.882812 12.316406 5.390625 11.738281 5.984375 C 11.148438 6.585938 10.730469 6.9375 10.011719 7.421875 L 9.484375 7.777344 L 9.722656 8.039062 C 9.847656 8.179688 9.957031 8.304688 9.957031 8.316406 C 9.957031 8.328125 9.824219 8.476562 9.667969 8.648438 C 9.109375 9.238281 8.539062 10.03125 8.316406 10.519531 C 8.113281 10.964844 8.113281 11.140625 8.351562 12.316406 C 8.410156 12.628906 8.460938 12.890625 8.457031 12.902344 C 8.453125 12.9375 8.308594 12.589844 8.214844 12.316406 C 7.90625 11.457031 7.824219 10.757812 7.9375 10.011719 C 8.039062 9.320312 8.269531 8.78125 8.796875 8.011719 C 8.992188 7.722656 9.03125 7.652344 8.996094 7.621094 C 8.863281 7.523438 3.875 3.300781 3.753906 3.1875 C 3.675781 3.113281 3.46875 2.953125 3.300781 2.835938 C 3.128906 2.722656 2.972656 2.589844 2.945312 2.550781 C 2.90625 2.492188 0.347656 0.304688 0.121094 0.136719 C 0.0898438 0.113281 0.0625 0.105469 0.0625 0.125 Z M 2.335938 2.472656 C 2.511719 2.625 2.964844 3.011719 3.339844 3.335938 C 3.714844 3.65625 4.296875 4.152344 4.628906 4.441406 C 4.960938 4.726562 5.917969 5.550781 6.757812 6.269531 C 7.597656 6.992188 8.3125 7.609375 8.347656 7.640625 C 8.402344 7.699219 8.402344 7.707031 8.179688 8.070312 C 8.058594 8.277344 7.90625 8.550781 7.839844 8.679688 C 7.390625 9.597656 7.28125 10.59375 7.511719 11.734375 C 7.699219 12.6875 8.042969 13.371094 8.816406 14.324219 C 9.121094 14.703125 9.203125 14.765625 9.175781 14.601562 C 9.167969 14.554688 9.125 14.226562 9.078125 13.878906 C 9.035156 13.53125 8.933594 12.832031 8.855469 12.328125 C 8.679688 11.1875 8.675781 11.046875 8.820312 10.742188 C 9.027344 10.300781 9.433594 9.734375 9.890625 9.257812 L 10.316406 8.804688 L 10.390625 8.898438 C 10.527344 9.0625 10.929688 9.695312 11.164062 10.109375 C 11.46875 10.640625 12.183594 12.132812 12.453125 12.792969 C 12.761719 13.539062 13.171875 14.632812 13.164062 14.667969 C 13.15625 14.6875 13.027344 14.503906 12.882812 14.265625 C 12.5625 13.75 12.121094 13.109375 11.863281 12.789062 C 11.316406 12.109375 10.765625 11.824219 10.269531 11.972656 C 10.171875 12.003906 10.0625 12.0625 10.011719 12.113281 C 9.847656 12.289062 9.828125 12.472656 9.828125 13.785156 L 9.828125 14.980469 L 10.042969 15.328125 C 10.449219 16.007812 10.914062 16.859375 11.210938 17.472656 C 11.53125 18.132812 12.097656 19.40625 12.082031 19.421875 C 12.074219 19.429688 11.847656 19.21875 11.574219 18.957031 C 9.042969 16.515625 7.855469 15.730469 5.359375 14.832031 C 5.183594 14.769531 5.027344 14.707031 5.011719 14.691406 C 4.984375 14.664062 5.949219 12.746094 5.992188 12.742188 C 6.035156 12.742188 6.625 13.179688 6.964844 13.46875 C 7.433594 13.859375 8.3125 14.707031 8.78125 15.210938 C 9.011719 15.464844 9.207031 15.660156 9.21875 15.652344 C 9.277344 15.59375 8.28125 14.320312 7.699219 13.707031 C 7.066406 13.042969 6.976562 12.964844 4.980469 11.476562 L 4.15625 10.863281 L 3.507812 9.425781 C 3.148438 8.640625 2.859375 7.988281 2.867188 7.984375 C 2.878906 7.96875 3.195312 8.242188 5.359375 10.097656 C 6.007812 10.660156 6.125 10.746094 6.144531 10.695312 C 6.171875 10.625 6.191406 10.179688 6.167969 10.164062 C 6 10.03125 3.277344 7.671875 3.160156 7.558594 C 3.007812 7.40625 2.957031 7.308594 2.402344 6.085938 C 2.074219 5.363281 1.8125 4.769531 1.820312 4.757812 C 1.835938 4.746094 2.148438 5.011719 5.851562 8.191406 C 6.210938 8.496094 6.273438 8.550781 6.300781 8.550781 C 6.316406 8.550781 6.332031 8.492188 6.332031 8.425781 C 6.332031 8.359375 6.339844 8.207031 6.347656 8.089844 L 6.371094 7.875 L 5.878906 7.457031 C 5.28125 6.945312 2.886719 4.886719 2.515625 4.566406 L 2.25 4.332031 L 1.539062 2.765625 C 1.148438 1.90625 0.828125 1.191406 0.828125 1.1875 C 0.828125 1.171875 1.414062 1.675781 2.335938 2.472656 Z M 25.554688 2.765625 L 24.835938 4.34375 L 23.515625 5.480469 C 22.789062 6.105469 21.871094 6.894531 21.476562 7.234375 L 20.753906 7.851562 L 20.761719 8.1875 C 20.765625 8.375 20.777344 8.53125 20.785156 8.542969 C 20.808594 8.566406 20.855469 8.53125 21.574219 7.910156 C 21.925781 7.601562 22.648438 6.980469 23.171875 6.535156 C 23.699219 6.085938 24.375 5.503906 24.679688 5.242188 C 24.980469 4.984375 25.238281 4.78125 25.246094 4.792969 C 25.261719 4.800781 25.246094 4.859375 25.21875 4.917969 C 25.191406 4.972656 24.921875 5.566406 24.621094 6.238281 L 24.066406 7.449219 L 23.839844 7.652344 C 23.71875 7.757812 23.085938 8.304688 22.4375 8.863281 C 21.792969 9.421875 21.230469 9.90625 21.183594 9.945312 C 21.140625 9.984375 21.058594 10.054688 21.003906 10.097656 L 20.898438 10.179688 L 20.917969 10.429688 C 20.925781 10.566406 20.941406 10.699219 20.949219 10.71875 C 20.957031 10.746094 21.183594 10.570312 21.527344 10.277344 C 21.835938 10.011719 22.570312 9.382812 23.15625 8.878906 C 23.742188 8.375 24.226562 7.96875 24.230469 7.976562 C 24.234375 7.980469 23.953125 8.621094 23.601562 9.398438 C 23.09375 10.527344 22.945312 10.828125 22.855469 10.910156 C 22.792969 10.960938 22.554688 11.148438 22.324219 11.320312 C 21.097656 12.230469 20.566406 12.636719 20.269531 12.878906 C 19.878906 13.195312 19.300781 13.769531 18.921875 14.222656 C 18.417969 14.828125 17.824219 15.625 17.859375 15.660156 C 17.867188 15.667969 18.199219 15.339844 18.597656 14.925781 C 19.417969 14.085938 19.890625 13.644531 20.421875 13.234375 C 20.9375 12.832031 21.089844 12.730469 21.125 12.765625 C 21.1875 12.832031 22.109375 14.671875 22.085938 14.691406 C 22.070312 14.707031 21.808594 14.804688 21.503906 14.921875 C 19.144531 15.789062 17.785156 16.71875 15.324219 19.144531 C 15.152344 19.3125 15.015625 19.433594 15.015625 19.414062 C 15.015625 19.363281 15.648438 17.941406 15.917969 17.394531 C 16.222656 16.777344 16.511719 16.242188 16.945312 15.511719 L 17.277344 14.949219 L 17.265625 13.707031 C 17.253906 12.632812 17.246094 12.445312 17.199219 12.320312 C 17.125 12.121094 17.035156 12.035156 16.847656 11.976562 C 16.277344 11.804688 15.730469 12.125 15.039062 13.035156 C 14.835938 13.304688 14.210938 14.242188 14.003906 14.59375 C 13.949219 14.691406 13.902344 14.757812 13.902344 14.742188 C 13.902344 14.6875 14.261719 13.722656 14.542969 13.035156 C 15.152344 11.539062 15.839844 10.175781 16.457031 9.257812 C 16.59375 9.046875 16.726562 8.859375 16.746094 8.839844 C 16.777344 8.816406 16.882812 8.90625 17.144531 9.1875 C 17.671875 9.746094 18.046875 10.269531 18.285156 10.757812 C 18.414062 11.035156 18.410156 11.199219 18.230469 12.367188 C 18.050781 13.546875 17.898438 14.691406 17.917969 14.714844 C 17.957031 14.753906 18.632812 13.894531 18.90625 13.472656 C 19.421875 12.648438 19.691406 11.675781 19.691406 10.605469 C 19.691406 9.679688 19.472656 8.976562 18.890625 8.035156 C 18.785156 7.871094 18.703125 7.71875 18.703125 7.703125 C 18.703125 7.679688 19.109375 7.320312 19.960938 6.59375 C 19.996094 6.5625 20.363281 6.25 20.773438 5.898438 C 21.183594 5.542969 21.785156 5.027344 22.109375 4.75 C 23.371094 3.667969 24.289062 2.875 25.253906 2.042969 C 25.808594 1.566406 26.261719 1.179688 26.265625 1.183594 C 26.273438 1.1875 25.949219 1.902344 25.554688 2.765625 Z M 13.941406 5.015625 C 14.785156 6.164062 15.671875 7.066406 16.523438 7.648438 C 16.679688 7.757812 16.816406 7.847656 16.820312 7.851562 C 16.828125 7.855469 16.738281 7.988281 16.625 8.144531 C 16.269531 8.652344 15.71875 9.507812 15.488281 9.921875 C 15.042969 10.714844 14.480469 11.976562 13.835938 13.644531 C 13.6875 14.023438 13.558594 14.332031 13.546875 14.335938 C 13.539062 14.335938 13.492188 14.226562 13.441406 14.097656 C 13.222656 13.523438 12.617188 12.035156 12.421875 11.589844 C 11.875 10.339844 11.488281 9.652344 10.601562 8.34375 C 10.421875 8.078125 10.273438 7.855469 10.273438 7.851562 C 10.273438 7.84375 10.386719 7.765625 10.523438 7.679688 C 11.421875 7.085938 12.375 6.101562 13.34375 4.753906 C 13.449219 4.613281 13.542969 4.5 13.558594 4.503906 C 13.566406 4.511719 13.742188 4.742188 13.941406 5.015625 Z M 10.882812 12.617188 C 11.402344 12.886719 12.308594 14.234375 13.179688 16.03125 C 13.375 16.441406 13.542969 16.773438 13.550781 16.773438 C 13.558594 16.769531 13.75 16.394531 13.972656 15.933594 C 14.585938 14.652344 15.277344 13.53125 15.789062 12.980469 C 16.066406 12.679688 16.265625 12.554688 16.507812 12.523438 C 16.636719 12.507812 16.660156 12.515625 16.683594 12.566406 C 16.726562 12.675781 16.738281 13.453125 16.707031 14.160156 L 16.675781 14.847656 L 16.25 15.605469 C 15.703125 16.589844 15.1875 17.632812 14.480469 19.164062 C 13.675781 20.90625 13.570312 21.128906 13.546875 21.128906 C 13.527344 21.128906 13.085938 20.207031 12.617188 19.179688 C 11.84375 17.476562 11.457031 16.703125 10.78125 15.503906 L 10.425781 14.871094 L 10.394531 14.320312 C 10.359375 13.679688 10.375 12.617188 10.414062 12.550781 C 10.453125 12.488281 10.710938 12.523438 10.882812 12.617188 Z M 10.882812 12.617188 \" />\n        {/* eslint-disable-next-line max-len */}\n        <path d=\"M 13.519531 5.386719 C 13.519531 5.402344 13.484375 5.761719 13.441406 6.191406 C 13.304688 7.535156 13.285156 7.988281 13.304688 9.28125 C 13.324219 10.484375 13.335938 10.757812 13.457031 12.050781 C 13.492188 12.429688 13.519531 12.765625 13.519531 12.796875 C 13.519531 12.832031 13.527344 12.839844 13.546875 12.820312 C 13.574219 12.792969 13.660156 11.894531 13.75 10.726562 C 13.769531 10.441406 13.785156 9.644531 13.785156 8.875 C 13.785156 7.734375 13.773438 7.414062 13.714844 6.808594 C 13.59375 5.574219 13.574219 5.363281 13.542969 5.363281 C 13.53125 5.363281 13.519531 5.375 13.519531 5.386719 Z M 13.519531 5.386719 \" />\n      </g>\n    </svg>\n  );\n};\n"
  },
  {
    "path": "web/src/components/sections/NavBar.tsx",
    "content": "import { Button, Flex } from '@chakra-ui/react';\nimport React from 'react';\nimport { Link } from 'react-router-dom';\nimport { userStore } from '../../lib/stores/userStore';\nimport { Logo } from '../common/Logo';\n\nexport const NavBar: React.FC = () => {\n  const current = userStore((state) => state.current);\n\n  return (\n    <Flex as=\"nav\" align=\"center\" justify=\"space-between\" wrap=\"wrap\" w=\"100%\" mb={8} p={8}>\n      <Flex align=\"center\">\n        <Logo />\n      </Flex>\n\n      <Flex align=\"center\" justify=\"flex-end\">\n        {current ? (\n          <Link to=\"/channels/me\">\n            <Button\n              _hover={{ bg: 'highlight.hover' }}\n              _active={{ bg: 'highlight.active' }}\n              _focus={{ boxShadow: 'none' }}\n              size=\"md\"\n              rounded=\"md\"\n              variant=\"outline\"\n            >\n              Open App\n            </Button>\n          </Link>\n        ) : (\n          <>\n            <Link to=\"/login\">\n              <Button\n                color=\"white\"\n                _hover={{ bg: 'highlight.hover' }}\n                _active={{ bg: 'highlight.active' }}\n                _focus={{ boxShadow: 'none' }}\n                size=\"md\"\n                rounded=\"md\"\n                variant=\"outline\"\n                mx=\"4\"\n              >\n                Login\n              </Button>\n            </Link>\n\n            <Link to=\"/register\">\n              <Button\n                _hover={{ bg: 'highlight.hover' }}\n                _active={{ bg: 'highlight.active' }}\n                _focus={{ boxShadow: 'none' }}\n                size=\"md\"\n                rounded=\"md\"\n                variant=\"outline\"\n              >\n                Register\n              </Button>\n            </Link>\n          </>\n        )}\n      </Flex>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/components/sections/OnlineLabel.tsx",
    "content": "import React from 'react';\nimport { Text } from '@chakra-ui/react';\n\ninterface LabelProps {\n  label: string;\n}\n\nexport const OnlineLabel: React.FC<LabelProps> = ({ label }) => (\n  <Text\n    fontSize=\"12px\"\n    color=\"brandGray.accent\"\n    textTransform=\"uppercase\"\n    fontWeight=\"semibold\"\n    mx=\"4\"\n    mt=\"4\"\n    mb=\"1\"\n    w=\"50%\"\n  >\n    {label}\n  </Text>\n);\n"
  },
  {
    "path": "web/src/components/sections/StartMessages.tsx",
    "content": "import { Avatar, Box, Divider, Flex, Heading, Text } from '@chakra-ui/react';\nimport React from 'react';\nimport { useParams } from 'react-router-dom';\nimport { useGetCurrentChannel } from '../../lib/utils/hooks/useGetCurrentChannel';\nimport { useGetCurrentDM } from '../../lib/utils/hooks/useGetCurrentDM';\nimport { RouterProps } from '../../lib/models/routerProps';\n\nexport const StartMessages: React.FC = () => {\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n  return guildId === undefined ? <DMStartMessages /> : <ChannelStartMessages />;\n};\n\nconst ChannelStartMessages: React.FC = () => {\n  const { guildId, channelId } = useParams<keyof RouterProps>() as RouterProps;\n  const channel = useGetCurrentChannel(channelId, guildId);\n\n  return (\n    <Flex alignItems=\"center\" mb=\"2\" justify=\"center\">\n      <Box textAlign=\"center\">\n        <Heading>Welcome to #{channel?.name}</Heading>\n        <Text>This is the start of the #{channel?.name} channel</Text>\n      </Box>\n    </Flex>\n  );\n};\n\nconst DMStartMessages: React.FC = () => {\n  const { channelId } = useParams<keyof RouterProps>() as RouterProps;\n  const channel = useGetCurrentDM(channelId);\n\n  return (\n    <Box m=\"4\">\n      <Box h=\"40px\" />\n      <Avatar h=\"80px\" w=\"80px\" src={channel?.user.image} />\n      <Heading mt={2}>{channel?.user.username}</Heading>\n      <Text textColor=\"brandGray.accent\">\n        This is the beginning of your direct message history with @{channel?.user.username}\n      </Text>\n      <Divider mt={2} />\n    </Box>\n  );\n};\n"
  },
  {
    "path": "web/src/components/sections/StyledTooltip.tsx",
    "content": "import React from 'react';\nimport { Tooltip } from '@chakra-ui/react';\n\ntype Placement = 'top' | 'right';\n\ninterface StyledTooltipProps {\n  label: string;\n  position: Placement;\n  disabled?: boolean;\n  children: React.ReactNode;\n}\n\nexport const StyledTooltip: React.FC<StyledTooltipProps> = ({ label, position, disabled = false, children }) => (\n  <Tooltip\n    hasArrow\n    label={label}\n    placement={position}\n    isDisabled={disabled}\n    bg=\"brandGray.darkest\"\n    color=\"white\"\n    fontWeight=\"semibold\"\n    py={1}\n    px={3}\n  >\n    {children}\n  </Tooltip>\n);\n"
  },
  {
    "path": "web/src/components/sections/UserPopover.tsx",
    "content": "import {\n  Avatar,\n  AvatarBadge,\n  Box,\n  Flex,\n  Popover,\n  PopoverContent,\n  PopoverFooter,\n  PopoverHeader,\n  PopoverTrigger,\n  Text,\n} from '@chakra-ui/react';\nimport React from 'react';\nimport { Member } from '../../lib/models/member';\n\ninterface UserPopoverProps {\n  member: Member;\n  children: React.ReactNode;\n}\n\nexport const UserPopover: React.FC<UserPopoverProps> = ({ member, children }) => (\n  <Popover isLazy placement=\"right-start\">\n    <PopoverTrigger>{children}</PopoverTrigger>\n    <PopoverContent w=\"80%\">\n      <PopoverHeader bg=\"brandGray.darker\" borderRadius=\"md\">\n        <Flex mt={2} align=\"center\" justify=\"center\">\n          <Box>\n            <Avatar src={member.image} size=\"xl\">\n              <AvatarBadge boxSize=\"0.9em\" bg={member.isOnline ? 'green.500' : 'gray.500'} />\n            </Avatar>\n            <Text mt={2} textAlign=\"center\" color=\"#fff\" fontWeight=\"semibold\">\n              {member.nickname ?? member.username}\n            </Text>\n            {member.nickname && <Text textAlign=\"center\">{member.username}</Text>}\n          </Box>\n        </Flex>\n      </PopoverHeader>\n      <PopoverFooter bg=\"brandGray.dark\">\n        <Text textColor=\"brandGray.accent\" fontSize=\"12px\" textAlign=\"center\">\n          Right click user for more actions\n        </Text>\n      </PopoverFooter>\n    </PopoverContent>\n  </Popover>\n);\n"
  },
  {
    "path": "web/src/index.tsx",
    "content": "import { ChakraProvider, ColorModeScript } from '@chakra-ui/react';\nimport * as React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { App } from './App';\nimport customTheme from './lib/utils/theme';\n\nconst container = document.getElementById('root')!;\nconst root = createRoot(container);\n\nroot.render(\n  <React.StrictMode>\n    <ColorModeScript />\n    <ChakraProvider theme={customTheme}>\n      <App />\n    </ChakraProvider>\n  </React.StrictMode>\n);\n"
  },
  {
    "path": "web/src/lib/api/dtos/AuthInput.ts",
    "content": "// eslint-disable-next-line max-classes-per-file\nexport interface LoginDTO {\n  email: string;\n  password: string;\n\n  [key: string]: any;\n}\n\nexport interface RegisterDTO extends LoginDTO {\n  username: string;\n}\n\nexport class ChangePasswordInput {\n  currentPassword!: string;\n\n  newPassword!: string;\n\n  confirmNewPassword!: string;\n}\n\nexport class ResetPasswordInput {\n  token!: string;\n\n  newPassword!: string;\n\n  confirmNewPassword!: string;\n}\n"
  },
  {
    "path": "web/src/lib/api/dtos/ChannelInput.ts",
    "content": "export type ChannelInput = {\n  name: string;\n  isPublic: boolean;\n  members?: string[];\n};\n"
  },
  {
    "path": "web/src/lib/api/dtos/GuildInput.ts",
    "content": "export type GuildInput = {\n  name: string;\n  image?: any;\n};\n"
  },
  {
    "path": "web/src/lib/api/dtos/GuildMemberInput.ts",
    "content": "export type GuildMemberInput = {\n  nickname?: string;\n  color?: string;\n};\n"
  },
  {
    "path": "web/src/lib/api/dtos/InviteInput.ts",
    "content": "export type InviteInput = {\n  link: string;\n};\n"
  },
  {
    "path": "web/src/lib/api/dtos/UserInput.ts",
    "content": "export type UpdateInput = Partial<{\n  email: string;\n  image: any;\n  username: string;\n}>;\n"
  },
  {
    "path": "web/src/lib/api/getSocket.ts",
    "content": "import ReconnectingWebSocket from 'reconnecting-websocket';\n\nexport const getSocket = (): ReconnectingWebSocket => new ReconnectingWebSocket(process.env.REACT_APP_WS!);\n\nlet socket: ReconnectingWebSocket | null = null;\nexport const getSameSocket = (): ReconnectingWebSocket => {\n  if (!socket) {\n    socket = new ReconnectingWebSocket(process.env.REACT_APP_WS!);\n  }\n\n  return socket;\n};\n"
  },
  {
    "path": "web/src/lib/api/handler/account.ts",
    "content": "import { AxiosResponse } from 'axios';\nimport { request } from '../setupAxios';\nimport { Account } from '../../models/account';\nimport { Member } from '../../models/member';\nimport { FriendRequest } from '../../models/friend';\n\nexport const getAccount = (): Promise<AxiosResponse<Account>> => request.get('/account');\n\nexport const updateAccount = (body: FormData): Promise<AxiosResponse<Account>> =>\n  request.put('/account', body, {\n    headers: {\n      'Content-Type': 'multipart/form-data',\n    },\n  });\n\nexport const getFriends = (): Promise<AxiosResponse<Member[]>> => request.get('/account/me/friends');\n\nexport const getPendingRequests = (): Promise<AxiosResponse<FriendRequest[]>> => request.get('/account/me/pending');\n\nexport const sendFriendRequest = (id: string): Promise<AxiosResponse<boolean>> => request.post(`/account/${id}/friend`);\n\nexport const acceptFriendRequest = (id: string): Promise<AxiosResponse<boolean>> =>\n  request.post(`/account/${id}/friend/accept`);\n\nexport const declineFriendRequest = (id: string): Promise<AxiosResponse<boolean>> =>\n  request.post(`/account/${id}/friend/cancel`);\n\nexport const removeFriend = (id: string): Promise<AxiosResponse<boolean>> => request.delete(`/account/${id}/friend`);\n"
  },
  {
    "path": "web/src/lib/api/handler/auth.ts",
    "content": "import { AxiosResponse } from 'axios';\nimport { request } from '../setupAxios';\nimport { ChangePasswordInput, LoginDTO, RegisterDTO, ResetPasswordInput } from '../dtos/AuthInput';\nimport { Account } from '../../models/account';\n\nexport const register = (body: RegisterDTO): Promise<AxiosResponse<Account>> => request.post('/account/register', body);\n\nexport const login = (body: LoginDTO): Promise<AxiosResponse<Account>> => request.post('/account/login', body);\n\nexport const logout = (): Promise<AxiosResponse> => request.post('/account/logout');\n\nexport const forgotPassword = (email: string): Promise<AxiosResponse<boolean>> =>\n  request.post('/account/forgot-password', { email });\n\nexport const changePassword = (body: ChangePasswordInput): Promise<AxiosResponse> =>\n  request.put('/account/change-password', body);\n\nexport const resetPassword = (body: ResetPasswordInput): Promise<AxiosResponse<Account>> =>\n  request.post('/account/reset-password', body);\n"
  },
  {
    "path": "web/src/lib/api/handler/channel.ts",
    "content": "import { AxiosResponse } from 'axios';\nimport { request } from '../setupAxios';\nimport { ChannelInput } from '../dtos/ChannelInput';\nimport { Channel } from '../../models/channel';\n\nexport const getChannels = (id: string): Promise<AxiosResponse<Channel[]>> => request.get(`channels/${id}`);\n\nexport const createChannel = (id: string, input: ChannelInput): Promise<AxiosResponse<Channel>> =>\n  request.post(`channels/${id}`, input);\n\nexport const editChannel = (channelId: string, input: ChannelInput): Promise<AxiosResponse<boolean>> =>\n  request.put(`channels/${channelId}`, input);\n\nexport const deleteChannel = (channelId: string): Promise<AxiosResponse<boolean>> =>\n  request.delete(`channels/${channelId}`);\n\nexport const getPrivateChannelMembers = (channelId: string): Promise<AxiosResponse<string[]>> =>\n  request.get(`channels/${channelId}/members`);\n"
  },
  {
    "path": "web/src/lib/api/handler/dm.ts",
    "content": "import { AxiosResponse } from 'axios';\nimport { request } from '../setupAxios';\nimport { DMChannel } from '../../models/dm';\n\nexport const getUserDMs = (): Promise<AxiosResponse<DMChannel[]>> => request.get('/channels/me/dm');\n\nexport const getOrCreateDirectMessage = (id: string): Promise<AxiosResponse<DMChannel>> =>\n  request.post(`/channels/${id}/dm`);\n\nexport const closeDirectMessage = (id: string): Promise<AxiosResponse<boolean>> => request.delete(`/channels/${id}/dm`);\n"
  },
  {
    "path": "web/src/lib/api/handler/guilds.ts",
    "content": "import { AxiosResponse } from 'axios';\nimport { request } from '../setupAxios';\nimport { GuildInput } from '../dtos/GuildInput';\nimport { InviteInput } from '../dtos/InviteInput';\nimport { Guild } from '../../models/guild';\nimport { Member } from '../../models/member';\nimport { VCMember } from '../../models/voice';\n\nexport const getUserGuilds = (): Promise<AxiosResponse<Guild[]>> => request.get('/guilds');\n\nexport const createGuild = (input: GuildInput): Promise<AxiosResponse<Guild>> => request.post('guilds/create', input);\n\nexport const joinGuild = (input: InviteInput): Promise<AxiosResponse<Guild>> => request.post('guilds/join', input);\n\nexport const getInviteLink = (id: string, isPermanent: boolean = false): Promise<AxiosResponse<string>> =>\n  request.get(`guilds/${id}/invite${isPermanent ? '?isPermanent=true' : ''}`);\n\nexport const invalidateInviteLinks = (id: string): Promise<AxiosResponse<boolean>> =>\n  request.delete(`guilds/${id}/invite`);\n\nexport const getGuildMembers = (id: string): Promise<AxiosResponse<Member[]>> => request.get(`guilds/${id}/members`);\n\nexport const getVCMembers = (id: string): Promise<AxiosResponse<VCMember[]>> => request.get(`guilds/${id}/vcmembers`);\n\nexport const leaveGuild = (id: string): Promise<AxiosResponse<boolean>> => request.delete(`guilds/${id}`);\n\nexport const editGuild = (id: string, input: FormData): Promise<AxiosResponse<boolean>> =>\n  request.put(`guilds/${id}`, input, {\n    headers: {\n      'Content-Type': 'multipart/form-data',\n    },\n  });\n\nexport const deleteGuild = (id: string): Promise<AxiosResponse<boolean>> => request.delete(`guilds/${id}/delete`);\n"
  },
  {
    "path": "web/src/lib/api/handler/members.ts",
    "content": "import { AxiosResponse } from 'axios';\nimport { GuildMemberInput } from '../dtos/GuildMemberInput';\nimport { request } from '../setupAxios';\nimport { Member } from '../../models/member';\n\nexport const getGuildMemberSettings = (id: string): Promise<AxiosResponse<GuildMemberInput>> =>\n  request.get(`guilds/${id}/member`);\n\nexport const changeGuildMemberSettings = (id: string, input: GuildMemberInput): Promise<AxiosResponse<boolean>> =>\n  request.put(`guilds/${id}/member`, input);\n\nexport const getBanList = (id: string): Promise<AxiosResponse<Member[]>> => request.get(`guilds/${id}/bans`);\n\nexport const kickMember = (guildId: string, memberId: string): Promise<AxiosResponse<boolean>> =>\n  request.post(`guilds/${guildId}/kick`, { memberId });\n\nexport const banMember = (guildId: string, memberId: string): Promise<AxiosResponse<boolean>> =>\n  request.post(`guilds/${guildId}/bans`, { memberId });\n\nexport const unbanMember = (guildId: string, memberId: string): Promise<AxiosResponse<boolean>> =>\n  request.delete(`guilds/${guildId}/bans`, { data: { memberId } });\n"
  },
  {
    "path": "web/src/lib/api/handler/messages.ts",
    "content": "import { AxiosResponse } from 'axios';\nimport { request } from '../setupAxios';\nimport { Message } from '../../models/message';\n\nexport const getMessages = (id: string, cursor?: string): Promise<AxiosResponse<Message[]>> =>\n  request.get(`messages/${id}${cursor ? `?cursor=${cursor}` : ''}`);\n\nexport const sendMessage = (\n  channelId: string,\n  data: FormData,\n  onUploadProgress?: (e: any) => void\n): Promise<AxiosResponse<void>> =>\n  request.post(`messages/${channelId}`, data, {\n    headers: {\n      'Content-Type': 'multipart/form-data',\n    },\n    onUploadProgress,\n  });\n\nexport const deleteMessage = (id: string): Promise<AxiosResponse<boolean>> => request.delete(`messages/${id}`);\n\nexport const editMessage = (id: string, text: string): Promise<AxiosResponse<boolean>> =>\n  request.put(`messages/${id}`, { text });\n"
  },
  {
    "path": "web/src/lib/api/setupAxios.ts",
    "content": "import Axios from 'axios';\n\nexport const request = Axios.create({\n  baseURL: `${process.env.REACT_APP_API!}/api`,\n  withCredentials: true,\n});\n"
  },
  {
    "path": "web/src/lib/api/ws/useChannelSocket.ts",
    "content": "import { useEffect } from 'react';\nimport { useNavigate, useLocation } from 'react-router-dom';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { getSocket } from '../getSocket';\nimport { useGetCurrentGuild } from '../../utils/hooks/useGetCurrentGuild';\nimport { userStore } from '../../stores/userStore';\nimport { Channel } from '../../models/channel';\nimport { VCMember, VoiceResponse } from '../../models/voice';\nimport { cKey, vcKey } from '../../utils/querykeys';\n\ntype WSMessage =\n  | { action: 'delete_channel' | 'new_notification'; data: string }\n  | { action: 'add_channel' | 'add_private_channel' | 'edit_channel'; data: Channel }\n  | { action: 'joinVoice' | 'leaveVoice'; data: VoiceResponse };\n\nexport function useChannelSocket(guildId: string): void {\n  const location = useLocation();\n  const navigate = useNavigate();\n  const cache = useQueryClient();\n  const guild = useGetCurrentGuild(guildId);\n  const current = userStore((state) => state.current);\n\n  useEffect((): any => {\n    const socket = getSocket();\n\n    socket.send(\n      JSON.stringify({\n        action: 'joinGuild',\n        room: guildId,\n      })\n    );\n    socket.send(\n      JSON.stringify({\n        action: 'joinUser',\n        room: current?.id,\n      })\n    );\n\n    const disconnect = (): void => {\n      socket.send(\n        JSON.stringify({\n          action: 'leaveGuild',\n          room: guildId,\n        })\n      );\n      socket.send(\n        JSON.stringify({\n          action: 'leaveRoom',\n          room: current?.id,\n        })\n      );\n      socket.close();\n    };\n\n    socket.addEventListener('message', (event) => {\n      const response: WSMessage = JSON.parse(event.data);\n      switch (response.action) {\n        case 'add_channel': {\n          cache.setQueryData<Channel[]>([cKey, guildId], (data) => [...(data ?? []), response.data]);\n          break;\n        }\n\n        case 'add_private_channel': {\n          cache.setQueryData<Channel[]>([cKey, guildId], (data) => [...(data ?? []), response.data]);\n          break;\n        }\n\n        case 'edit_channel': {\n          const editedChannel = response.data;\n          cache.setQueryData<Channel[]>([cKey, guildId], (d) => {\n            const data = d ?? [];\n\n            const contains = data.includes(editedChannel);\n\n            // Channel used to be private and is public now\n            if (!contains && editedChannel.isPublic) {\n              return [...data, editedChannel];\n            }\n\n            return data.map((c) => (c.id === editedChannel.id ? editedChannel : c));\n          });\n          break;\n        }\n\n        case 'delete_channel': {\n          const deleteId = response.data;\n          cache.setQueryData<Channel[]>([cKey, guildId], (d) => {\n            const currentPath = `/channels/${guildId}/${deleteId}`;\n\n            // The deleted channel is the channel the user is currently in\n            if (location.pathname === currentPath && guild) {\n              // If it's the default channel, redirect to home\n              if (deleteId === guild.default_channel_id) {\n                navigate('/channels/me', { replace: true });\n                // Redirect the user to the default channel\n              } else {\n                navigate(`/channels/${guild.id}/${guild.default_channel_id}`, { replace: true });\n              }\n            }\n            return d?.filter((c) => c.id !== deleteId) ?? [];\n          });\n          break;\n        }\n\n        case 'new_notification': {\n          const id = response.data;\n          const currentPath = `/channels/${guildId}/${id}`;\n          if (location.pathname !== currentPath) {\n            cache.setQueryData<Channel[]>(\n              [cKey, guildId],\n              (d) => d?.map((c) => (c.id === id ? { ...c, hasNotification: true } : c)) ?? []\n            );\n          }\n          break;\n        }\n\n        case 'joinVoice': {\n          const { data } = response;\n          // Remove the current user from the list\n          cache.setQueryData<VCMember[]>([vcKey, guildId], (_) => data.clients.filter((e) => e.id !== current?.id));\n          break;\n        }\n\n        case 'leaveVoice': {\n          const { data } = response;\n          // Remove the current user from the list\n          cache.setQueryData<VCMember[]>([vcKey, guildId], (_) => data.clients.filter((e) => e.id !== current?.id));\n          break;\n        }\n\n        default:\n          break;\n      }\n    });\n\n    window.addEventListener('beforeunload', disconnect);\n\n    return () => disconnect();\n  }, [guildId, cache, navigate, location, guild, current]);\n}\n"
  },
  {
    "path": "web/src/lib/api/ws/useDMSocket.ts",
    "content": "import { useEffect } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { getSocket } from '../getSocket';\nimport { userStore } from '../../stores/userStore';\nimport { dmKey } from '../../utils/querykeys';\nimport { DMChannel } from '../../models/dm';\n\ntype WSMessage = { action: 'push_to_top'; data: string };\n\nexport function useDMSocket(): void {\n  const current = userStore((state) => state.current);\n  const cache = useQueryClient();\n\n  useEffect(() => {\n    const socket = getSocket();\n\n    socket.send(\n      JSON.stringify({\n        action: 'joinUser',\n        room: current?.id,\n      })\n    );\n\n    socket.addEventListener('message', (event) => {\n      const response: WSMessage = JSON.parse(event.data);\n\n      switch (response.action) {\n        case 'push_to_top': {\n          const dmId = response.data;\n          cache.setQueryData<DMChannel[]>([dmKey], (d) => {\n            const data = d ?? [];\n            const index = data.findIndex((dm) => dm.id === dmId);\n\n            // If no DM exists or it's already the top one, do nothing\n            if (index === 0 || index === -1) return [...data];\n\n            // Push the DM to the top\n            const dm = data[index];\n            return [dm, ...data.filter((dc) => dc.id !== dmId)];\n          });\n          break;\n        }\n\n        default:\n          break;\n      }\n    });\n\n    return () => {\n      socket.send(\n        JSON.stringify({\n          action: 'leaveRoom',\n          room: current?.id,\n        })\n      );\n      socket.close();\n    };\n  }, [current, cache]);\n}\n"
  },
  {
    "path": "web/src/lib/api/ws/useFriendSocket.ts",
    "content": "import { useEffect } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { getSocket } from '../getSocket';\nimport { userStore } from '../../stores/userStore';\nimport { fKey } from '../../utils/querykeys';\nimport { homeStore } from '../../stores/homeStore';\nimport { Friend } from '../../models/friend';\n\ntype WSMessage =\n  | { action: 'toggle_online' | 'toggle_offline' | 'remove_friend'; data: string }\n  | { action: 'requestCount'; data: number }\n  | { action: 'add_friend'; data: Friend };\n\nexport function useFriendSocket(): void {\n  const current = userStore((state) => state.current);\n  const setRequests = homeStore((state) => state.setRequests);\n  const cache = useQueryClient();\n\n  useEffect((): any => {\n    const socket = getSocket();\n    socket.send(\n      JSON.stringify({\n        action: 'joinUser',\n        room: current?.id,\n      })\n    );\n\n    socket.send(JSON.stringify({ action: 'getRequestCount' }));\n\n    socket.addEventListener('message', (event) => {\n      const response: WSMessage = JSON.parse(event.data);\n      switch (response.action) {\n        case 'toggle_online': {\n          cache.setQueryData<Friend[]>([fKey], (d) => {\n            if (!d) return [];\n            return d.map((f) => (f.id === response.data ? { ...f, isOnline: true } : f));\n          });\n          break;\n        }\n\n        case 'toggle_offline': {\n          cache.setQueryData<Friend[]>([fKey], (d) => {\n            if (!d) return [];\n            return d.map((f) => (f.id === response.data ? { ...f, isOnline: false } : f));\n          });\n          break;\n        }\n\n        case 'requestCount': {\n          setRequests(response.data);\n          break;\n        }\n\n        case 'add_friend': {\n          cache.setQueryData<Friend[]>([fKey], (data) =>\n            [...(data ?? []), response.data].sort((a, b) => a.username.localeCompare(b.username))\n          );\n          break;\n        }\n\n        case 'remove_friend': {\n          cache.setQueryData<Friend[]>([fKey], (data) => [...(data?.filter((m) => m.id !== response.data) ?? [])]);\n          break;\n        }\n\n        default:\n          break;\n      }\n    });\n\n    return () => {\n      socket.send(\n        JSON.stringify({\n          action: 'leaveRoom',\n          room: current?.id,\n        })\n      );\n\n      socket.close();\n    };\n  }, [cache, current, setRequests]);\n}\n"
  },
  {
    "path": "web/src/lib/api/ws/useGuildSocket.ts",
    "content": "import { useEffect } from 'react';\nimport { useNavigate, useLocation } from 'react-router-dom';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { getSocket } from '../getSocket';\nimport { userStore } from '../../stores/userStore';\nimport { gKey } from '../../utils/querykeys';\nimport { Guild } from '../../models/guild';\n\ntype WSMessage =\n  | { action: 'delete_guild' | 'remove_from_guild' | 'new_notification'; data: string }\n  | { action: 'edit_guild'; data: Guild };\n\nexport function useGuildSocket(): void {\n  const navigate = useNavigate();\n  const cache = useQueryClient();\n  const current = userStore((state) => state.current);\n  const location = useLocation();\n\n  useEffect((): any => {\n    const socket = getSocket();\n\n    socket.send(\n      JSON.stringify({\n        action: 'joinUser',\n        room: current?.id,\n      })\n    );\n\n    socket.addEventListener('message', (event) => {\n      const response: WSMessage = JSON.parse(event.data);\n      switch (response.action) {\n        case 'edit_guild': {\n          const editedGuild = response.data;\n          cache.setQueryData<Guild[]>([gKey], (d) => {\n            if (!d) return [];\n            return d.map((g) =>\n              g.id === editedGuild.id\n                ? {\n                    ...g,\n                    name: editedGuild.name,\n                    icon: editedGuild.icon,\n                  }\n                : g\n            );\n          });\n          break;\n        }\n\n        case 'delete_guild': {\n          const deleteId = response.data;\n          cache.setQueryData<Guild[]>([gKey], (d) => {\n            const isActive = location.pathname.includes(deleteId);\n            if (isActive) {\n              navigate('/channels/me', { replace: true });\n            }\n            return d?.filter((g) => g.id !== deleteId) ?? [];\n          });\n          break;\n        }\n\n        case 'new_notification': {\n          const id = response.data;\n          if (!location.pathname.includes(id)) {\n            cache.setQueryData<Guild[]>([gKey], (d) => {\n              if (!d) return [];\n              return d.map((g) =>\n                g.id === id\n                  ? {\n                      ...g,\n                      hasNotification: true,\n                    }\n                  : g\n              );\n            });\n          }\n          break;\n        }\n\n        case 'remove_from_guild': {\n          cache.setQueryData<Guild[]>([gKey], (d) => {\n            const guildId = response.data;\n            const isActive = location.pathname.includes(guildId);\n            if (isActive) {\n              navigate('/channels/me', { replace: true });\n            }\n            return d?.filter((g) => g.id !== guildId) ?? [];\n          });\n          break;\n        }\n\n        default:\n          break;\n      }\n    });\n\n    return () => {\n      socket.send(\n        JSON.stringify({\n          action: 'leaveRoom',\n          room: current?.id,\n        })\n      );\n      socket.close();\n    };\n  }, [current, cache, navigate, location]);\n}\n"
  },
  {
    "path": "web/src/lib/api/ws/useMemberSocket.ts",
    "content": "import { useEffect } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { getSocket } from '../getSocket';\nimport { Member } from '../../models/member';\nimport { mKey } from '../../utils/querykeys';\n\ntype WSMessage =\n  | { action: 'remove_member' | 'toggle_online' | 'toggle_offline'; data: string }\n  | { action: 'add_member'; data: Member };\n\nexport function useMemberSocket(guildId: string): void {\n  const cache = useQueryClient();\n\n  useEffect((): any => {\n    const socket = getSocket();\n    const key = [mKey, guildId];\n\n    socket.send(\n      JSON.stringify({\n        action: 'joinGuild',\n        room: guildId,\n      })\n    );\n\n    socket.addEventListener('message', (event) => {\n      const response: WSMessage = JSON.parse(event.data);\n      switch (response.action) {\n        case 'add_member': {\n          cache.setQueryData<Member[]>(key, (data) =>\n            // Add member and sort array by nickname, then username\n            [...(data ?? []), response.data].sort((a, b) => {\n              if (a.nickname && b.nickname) {\n                return a.nickname.localeCompare(b.nickname);\n              }\n              if (a.nickname && !b.nickname) {\n                return a.nickname.localeCompare(b.username);\n              }\n              if (!a.nickname && b.nickname) {\n                return a.username.localeCompare(b.nickname);\n              }\n              return a.username.localeCompare(b.username);\n            })\n          );\n          break;\n        }\n\n        case 'remove_member': {\n          cache.setQueryData<Member[]>(key, (data) => [...(data?.filter((m) => m.id !== response.data) ?? [])]);\n          break;\n        }\n\n        case 'toggle_online': {\n          const memberId = response.data;\n          cache.setQueryData<Member[]>(key, (d) => {\n            if (!d) return [];\n            return d.map((m) => (m.id === memberId ? { ...m, isOnline: true } : m));\n          });\n          break;\n        }\n\n        case 'toggle_offline': {\n          const memberId = response.data;\n          cache.setQueryData<Member[]>(key, (d) => {\n            if (!d) return [];\n            return d.map((m) => (m.id === memberId ? { ...m, isOnline: false } : m));\n          });\n          break;\n        }\n\n        default:\n          break;\n      }\n    });\n\n    return () => {\n      socket.send(\n        JSON.stringify({\n          action: 'leaveRoom',\n          room: guildId,\n        })\n      );\n      socket.close();\n    };\n  }, [cache, guildId]);\n}\n"
  },
  {
    "path": "web/src/lib/api/ws/useMessageSocket.ts",
    "content": "import { useEffect } from 'react';\nimport { InfiniteData, useQueryClient } from '@tanstack/react-query';\nimport { getSocket } from '../getSocket';\nimport { userStore } from '../../stores/userStore';\nimport { channelStore } from '../../stores/channelStore';\nimport { Message } from '../../models/message';\nimport { msgKey } from '../../utils/querykeys';\n\ntype WSMessage =\n  | { action: 'new_message' | 'edit_message'; data: Message }\n  | { action: 'addToTyping' | 'removeFromTyping' | 'delete_message'; data: string };\n\nexport function useMessageSocket(channelId: string): void {\n  const current = userStore((state) => state.current);\n  const store = channelStore();\n  const cache = useQueryClient();\n\n  useEffect((): any => {\n    store.reset();\n    const socket = getSocket();\n\n    socket.send(\n      JSON.stringify({\n        action: 'joinChannel',\n        room: channelId,\n      })\n    );\n\n    socket.addEventListener('message', (event) => {\n      const response: WSMessage = JSON.parse(event.data);\n      switch (response.action) {\n        case 'new_message': {\n          cache.setQueryData<InfiniteData<Message[]>>([msgKey, channelId], (d) => {\n            if (!d) return { pages: [], pageParams: [] };\n            return {\n              pages: d.pages.map((messages, i) => (i === 0 ? [response.data, ...messages] : messages)),\n              pageParams: [...d.pageParams],\n            };\n          });\n          break;\n        }\n\n        case 'edit_message': {\n          const editMessage = response.data;\n          cache.setQueryData<InfiniteData<Message[]>>([msgKey, channelId], (d) => {\n            if (!d) return { pages: [], pageParams: [] };\n            return {\n              pages: d.pages.map((messages) =>\n                messages.map((m) =>\n                  m.id === editMessage.id ? { ...m, text: editMessage.text, updatedAt: editMessage.updatedAt } : m\n                )\n              ),\n              pageParams: [...d.pageParams],\n            };\n          });\n          break;\n        }\n\n        case 'delete_message': {\n          const messageId = response.data;\n          cache.setQueryData<InfiniteData<Message[]>>([msgKey, channelId], (d) => {\n            if (!d) return { pages: [], pageParams: [] };\n            return {\n              pages: d.pages.map((messages) => messages.filter((m) => m.id !== messageId)),\n              pageParams: [...d.pageParams],\n            };\n          });\n          break;\n        }\n\n        case 'addToTyping': {\n          const username = response.data;\n          if (username !== current?.username) store.addTyping(username);\n          break;\n        }\n\n        case 'removeFromTyping': {\n          const username = response.data;\n          if (username !== current?.username) store.removeTyping(username);\n          break;\n        }\n\n        default:\n          break;\n      }\n    });\n\n    return () => {\n      socket.send(\n        JSON.stringify({\n          action: 'leaveRoom',\n          room: channelId,\n        })\n      );\n\n      socket.close();\n    };\n    // eslint-disable-next-line\n  }, [channelId, cache, current]);\n}\n"
  },
  {
    "path": "web/src/lib/api/ws/useRequestSocket.ts",
    "content": "import { useEffect } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { getSocket } from '../getSocket';\nimport { userStore } from '../../stores/userStore';\nimport { rKey } from '../../utils/querykeys';\nimport { homeStore } from '../../stores/homeStore';\nimport { FriendRequest } from '../../models/friend';\n\ntype WSMessage = { action: 'add_request'; data: FriendRequest };\n\nexport function useRequestSocket(): void {\n  const current = userStore((state) => state.current);\n  const setRequests = homeStore((state) => state.setRequests);\n  const cache = useQueryClient();\n\n  useEffect((): any => {\n    const socket = getSocket();\n\n    socket.send(\n      JSON.stringify({\n        action: 'joinUser',\n        room: current?.id,\n      })\n    );\n\n    socket.addEventListener('message', (event) => {\n      const response: WSMessage = JSON.parse(event.data);\n      switch (response.action) {\n        case 'add_request': {\n          cache.setQueryData<FriendRequest[]>([rKey], (data) =>\n            [...(data ?? []), response.data].sort((a, b) => a.username.localeCompare(b.username))\n          );\n          break;\n        }\n\n        default:\n          break;\n      }\n    });\n\n    return () => {\n      socket.send(\n        JSON.stringify({\n          action: 'leaveRoom',\n          room: current?.id,\n        })\n      );\n      socket.close();\n    };\n  }, [cache, current, setRequests]);\n}\n"
  },
  {
    "path": "web/src/lib/api/ws/useVoiceSocket.ts",
    "content": "import { useEffect } from 'react';\nimport { useQueryClient } from '@tanstack/react-query';\nimport { useParams } from 'react-router-dom';\nimport { RouterProps } from '../../models/routerProps';\nimport { VCMember, VoiceResponse, VoiceSignal } from '../../models/voice';\nimport { userStore } from '../../stores/userStore';\nimport { voiceStore } from '../../stores/voiceStore';\nimport { getSameSocket } from '../getSocket';\nimport { vcKey } from '../../utils/querykeys';\n\ntype WSMessage =\n  | { action: 'joinVoice' | 'leaveVoice'; data: VoiceResponse }\n  | { action: 'toggle-mute' | 'toggle-deafen'; data: { id: string; value: boolean } }\n  | { action: 'voice-signal'; data: VoiceSignal };\n\nexport function useVoiceSocket(): void {\n  const { guildId } = useParams<keyof RouterProps>() as RouterProps;\n  const key = [vcKey, guildId];\n\n  const current = userStore((state) => state.current);\n\n  const [inVC, setVoiceClients, setVoiceJoinUserId, setRtcSignalData, setVoiceLeaveUserId] = voiceStore((state) => [\n    state.inVC,\n    state.setVoiceClients,\n    state.setVoiceJoinUserId,\n    state.setRtcSignalData,\n    state.setVoiceLeaveUserId,\n  ]);\n\n  const cache = useQueryClient();\n  const socket = getSameSocket();\n\n  useEffect(() => {\n    if (inVC) {\n      socket.send(\n        JSON.stringify({\n          action: 'joinVoice',\n          room: guildId,\n        })\n      );\n\n      const disconnect = (): void => {\n        socket.send(\n          JSON.stringify({\n            action: 'leaveVoice',\n            room: guildId,\n          })\n        );\n        socket.close();\n      };\n\n      socket.addEventListener('message', (event) => {\n        const response: WSMessage = JSON.parse(event.data);\n        switch (response.action) {\n          case 'joinVoice': {\n            const { data } = response;\n            setVoiceClients(data.clients);\n\n            // Remove the current user from the list\n            cache.setQueryData<VCMember[]>(key, (_) => data.clients.filter((e) => e.id !== current?.id));\n\n            if (inVC) {\n              setVoiceJoinUserId(data.userId);\n            }\n            break;\n          }\n\n          case 'leaveVoice': {\n            const { data } = response;\n            setVoiceClients(data.clients);\n\n            // Remove the current user from the list\n            cache.setQueryData<VCMember[]>(key, (_) => data.clients.filter((e) => e.id !== current?.id));\n\n            if (inVC) {\n              setVoiceLeaveUserId(data.userId);\n            }\n            break;\n          }\n\n          case 'voice-signal': {\n            if (inVC) {\n              const { data } = response;\n              setRtcSignalData(data);\n            }\n            break;\n          }\n\n          // For unknown reasons voiceClients is empty\n          case 'toggle-mute': {\n            // const { data } = response;\n            // const clients = voiceClients.map((e) => {\n            //   if (e.id === data.id) {\n            //     return { ...e, isMuted: data.value };\n            //   }\n            //   return e;\n            // });\n\n            // setVoiceClients(clients);\n            // setVoiceMembers(clients);\n            break;\n          }\n\n          // Same here\n          case 'toggle-deafen': {\n            // const { data } = response;\n            // const clients = voiceClients.map((e) => {\n            //   if (e.id === data.id) {\n            //     return { ...e, isDeafened: data.value };\n            //   }\n            //   return e;\n            // });\n\n            // setVoiceClients(clients);\n            // setVoiceMembers(clients);\n            break;\n          }\n\n          default:\n            break;\n        }\n      });\n\n      window.addEventListener('beforeunload', disconnect);\n\n      return () => disconnect();\n    }\n\n    return undefined;\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [inVC, socket]);\n}\n"
  },
  {
    "path": "web/src/lib/models/account.ts",
    "content": "export interface Account {\n  id: string;\n  username: string;\n  email: string;\n  image: string;\n}\n"
  },
  {
    "path": "web/src/lib/models/channel.ts",
    "content": "export interface Channel {\n  id: string;\n  name: string;\n  isPublic: boolean;\n  hasNotification?: boolean;\n}\n"
  },
  {
    "path": "web/src/lib/models/dm.ts",
    "content": "export interface DMChannel {\n  id: string;\n  user: DMMember;\n}\n\nexport interface DMNotification extends DMChannel {\n  count: number;\n}\n\nexport interface DMMember {\n  id: string;\n  username: string;\n  image: string;\n  isOnline: boolean;\n  isFriend: boolean;\n}\n"
  },
  {
    "path": "web/src/lib/models/fieldError.ts",
    "content": "export interface FieldError {\n  field: string;\n  message: string;\n}\n"
  },
  {
    "path": "web/src/lib/models/friend.ts",
    "content": "export interface Friend {\n  id: string;\n  username: string;\n  image: string;\n  isOnline: boolean;\n}\n\nexport enum RequestType {\n  OUTGOING,\n  INCOMING,\n}\n\nexport interface FriendRequest {\n  id: string;\n  username: string;\n  image: string;\n  type: RequestType;\n}\n"
  },
  {
    "path": "web/src/lib/models/guild.ts",
    "content": "export interface Guild {\n  id: string;\n  name: string;\n  ownerId: string;\n  default_channel_id: string;\n  icon?: string;\n  hasNotification?: boolean;\n}\n"
  },
  {
    "path": "web/src/lib/models/member.ts",
    "content": "export interface Member {\n  id: string;\n  username: string;\n  image: string;\n  isOnline: boolean;\n  isFriend: boolean;\n  nickname?: string | null;\n  color?: string | null;\n}\n"
  },
  {
    "path": "web/src/lib/models/message.ts",
    "content": "import { Member } from './member';\n\nexport interface Message {\n  id: string;\n  text?: string;\n  createdAt: string;\n  updatedAt: string;\n  attachment?: Attachment;\n  user: Member;\n}\n\nexport interface Attachment {\n  filename: string;\n  filetype: string;\n  url: string;\n}\n"
  },
  {
    "path": "web/src/lib/models/routerProps.ts",
    "content": "export type RouterProps = {\n  guildId: string;\n  channelId: string;\n};\n"
  },
  {
    "path": "web/src/lib/models/voice.ts",
    "content": "export interface VoiceSignal {\n  userId: string;\n  sdp?: RTCSessionDescription | null;\n  ice?: RTCIceCandidate | null;\n}\n\nexport interface VoiceResponse {\n  clients: VCMember[];\n  userId: string;\n}\n\nexport interface VCMember {\n  id: string;\n  username: string;\n  image: string;\n  nickname?: string | null;\n  isMuted: boolean;\n  isDeafened: boolean;\n  stream?: MediaStream | null;\n}\n"
  },
  {
    "path": "web/src/lib/stores/channelStore.ts",
    "content": "import create from 'zustand';\n\ntype ChannelState = {\n  typing: string[];\n  addTyping: (username: string) => void;\n  removeTyping: (username: string) => void;\n  reset: () => void;\n};\n\nexport const channelStore = create<ChannelState>((set) => ({\n  typing: [],\n  addTyping: (username) => set((state) => ({ typing: [...state.typing, username] })),\n  removeTyping: (username) => set((state) => ({ typing: [...state.typing.filter((u) => u !== username)] })),\n  reset: () => set({ typing: [] }),\n}));\n"
  },
  {
    "path": "web/src/lib/stores/homeStore.ts",
    "content": "import create from 'zustand';\n\ntype HomeStoreType = {\n  notifCount: number;\n  requestCount: number;\n  increment: () => void;\n  setRequests: (r: number) => void;\n  reset: () => void;\n  resetRequest: () => void;\n  isPending: boolean;\n  toggleDisplay: () => void;\n};\n\nexport const homeStore = create<HomeStoreType>((set) => ({\n  notifCount: 0,\n  requestCount: 0,\n  increment: () => set((state) => ({ notifCount: state.notifCount + 1 })),\n  reset: () => set({ notifCount: 0 }),\n  resetRequest: () => set({ requestCount: 0 }),\n  setRequests: (r) => set({ requestCount: r }),\n  isPending: false,\n  toggleDisplay: () => set((state) => ({ isPending: !state.isPending })),\n}));\n"
  },
  {
    "path": "web/src/lib/stores/settingsStore.ts",
    "content": "import create from 'zustand';\nimport { persist } from 'zustand/middleware';\n\ntype SettingsState = {\n  showMembers: boolean;\n  toggleShowMembers: () => void;\n};\n\nexport const settingsStore = create(\n  persist<SettingsState>(\n    (set, get) => ({\n      showMembers: true,\n      toggleShowMembers: () => set({ showMembers: !get().showMembers }),\n    }),\n    {\n      name: 'settings-storage',\n    }\n  )\n);\n"
  },
  {
    "path": "web/src/lib/stores/userStore.ts",
    "content": "import create from 'zustand';\nimport { persist } from 'zustand/middleware';\nimport { Account } from '../models/account';\n\ntype AccountState = {\n  current: Account | null;\n  setUser: (account: Account) => void;\n  logout: () => void;\n};\n\nexport const userStore = create(\n  persist<AccountState>(\n    (set, _) => ({\n      current: null,\n      setUser: (account) => set({ current: account }),\n      logout: () => set({ current: null }),\n    }),\n    {\n      name: 'user-storage',\n    }\n  )\n);\n"
  },
  {
    "path": "web/src/lib/stores/voiceStore.ts",
    "content": "import create from 'zustand';\nimport { VCMember, VoiceSignal } from '../models/voice';\n\ntype VoiceState = {\n  voiceChatID: string;\n  inVC: boolean;\n  voiceClients: VCMember[];\n  voiceJoinUserId: string;\n  voiceLeaveUserId: string;\n  localStream: MediaStream | null;\n  connections: Map<string, RTCPeerConnection>;\n  rtcSignalData: VoiceSignal;\n  isMuted: boolean;\n  isDeafened: boolean;\n  setVoiceID: (id: string) => void;\n  setVoiceClients: (clients: VCMember[]) => void;\n  setInVC: (value: boolean) => void;\n  setVoiceJoinUserId: (id: string) => void;\n  setVoiceLeaveUserId: (id: string) => void;\n  setLocalStream: (stream: MediaStream) => void;\n  setRtcSignalData: (signal: VoiceSignal) => void;\n  getConnection: (userId: string) => RTCPeerConnection | undefined;\n  setConnection: (userId: string, connection: RTCPeerConnection) => void;\n  deleteConnection: (userId: string) => void;\n  clearConnections: () => void;\n  setIsMuted: (value: boolean) => void;\n  setIsDeafened: (value: boolean) => void;\n  leaveVoice: () => void;\n};\n\nexport const voiceStore = create<VoiceState>((set, get) => ({\n  voiceChatID: '',\n  voiceClients: [],\n  inVC: false,\n  voiceJoinUserId: '',\n  voiceLeaveUserId: '',\n  localStream: null,\n  connections: new Map<string, RTCPeerConnection>(),\n  rtcSignalData: { userId: '' },\n  isMuted: false,\n  isDeafened: false,\n  setVoiceID: (id) => set({ voiceChatID: id }),\n  setInVC: (value) => set({ inVC: value }),\n  setVoiceClients: (clients) => set({ voiceClients: clients }),\n  setVoiceJoinUserId: (id) => set({ voiceJoinUserId: id }),\n  setVoiceLeaveUserId: (id) => set({ voiceLeaveUserId: id }),\n  setLocalStream: (stream) => set({ localStream: stream }),\n  setRtcSignalData: (signal) => set({ rtcSignalData: signal }),\n  getConnection: (userId) => get().connections.get(userId),\n  setConnection: (userId, connection) => get().connections.set(userId, connection),\n  deleteConnection: (userId) => get().connections.delete(userId),\n  clearConnections: () => {\n    get().connections.forEach((v, _) => {\n      v.close();\n    });\n\n    get().connections.clear();\n  },\n  setIsDeafened: (value) => {\n    const stream = get().localStream;\n    if (stream) stream.getAudioTracks()[0].enabled = !value;\n    set({ isDeafened: value, localStream: stream });\n  },\n  setIsMuted: (value) => {\n    const stream = get().localStream;\n    if (stream && !get().isDeafened) stream.getAudioTracks()[0].enabled = !value;\n    set({ isMuted: value, localStream: stream });\n  },\n  leaveVoice: () => {\n    get().clearConnections();\n    set({\n      inVC: false,\n      voiceClients: [],\n      voiceJoinUserId: '',\n      voiceLeaveUserId: '',\n      voiceChatID: '',\n    });\n  },\n}));\n"
  },
  {
    "path": "web/src/lib/utils/cropImage.ts",
    "content": "const createImage = (url: string): Promise<HTMLImageElement> =>\n  new Promise((resolve, reject) => {\n    const image = new Image();\n    image.addEventListener('load', () => resolve(image));\n    image.addEventListener('error', (error) => reject(error));\n    image.setAttribute('crossOrigin', 'anonymous'); // needed to avoid cross-origin issues on CodeSandbox\n    image.src = url;\n  });\n\n/**\n * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop\n * @param {File} imageSrc - Image File url\n * @param {Object} pixelCrop - pixelCrop Object provided by react-easy-crop\n * @param {number} rotation - optional rotation parameter\n */\nexport default async function getCroppedImg(imageSrc: string, pixelCrop: any): Promise<Blob> {\n  const image = await createImage(imageSrc);\n  const canvas = document.createElement('canvas');\n  const ctx = canvas.getContext('2d');\n\n  const maxSize = Math.max(image.width, image.height);\n  const safeArea = 2 * ((maxSize / 2) * Math.sqrt(2));\n\n  // set each dimensions to double largest dimension to allow for a safe area for the\n  // image to rotate in without being clipped by canvas context\n  canvas.width = safeArea;\n  canvas.height = safeArea;\n\n  // translate canvas context to a central location on image to allow rotating around the center.\n  ctx!.translate(safeArea / 2, safeArea / 2);\n  ctx!.translate(-safeArea / 2, -safeArea / 2);\n\n  // draw rotated image and store data.\n  ctx!.drawImage(image, safeArea / 2 - image.width * 0.5, safeArea / 2 - image.height * 0.5);\n  const data = ctx!.getImageData(0, 0, safeArea, safeArea);\n\n  // set canvas width to final desired crop size - this will clear existing context\n  canvas.width = pixelCrop.width;\n  canvas.height = pixelCrop.height;\n\n  // paste generated rotate image with correct offsets for x,y crop values.\n  ctx!.putImageData(\n    data,\n    Math.round(0 - safeArea / 2 + image.width * 0.5 - pixelCrop.x),\n    Math.round(0 - safeArea / 2 + image.height * 0.5 - pixelCrop.y)\n  );\n\n  // As Base64 string\n  // return canvas.toDataURL('image/jpeg');\n\n  // As a blob\n  return new Promise((resolve) => {\n    canvas.toBlob((file) => {\n      resolve(file!);\n    }, 'image/jpeg');\n  });\n}\n"
  },
  {
    "path": "web/src/lib/utils/dateUtils.ts",
    "content": "import dayjs from 'dayjs';\nimport calender from 'dayjs/plugin/calendar';\n\ndayjs.extend(calender);\n\nexport const getTime = (createdAt: string): string => dayjs(createdAt).calendar();\n\nexport const getShortenedTime = (createdAt: string): string => dayjs(createdAt).format('h:mm A');\n\nexport const getTimeDifference = (date1: string, date2: string): number => dayjs(date1).diff(dayjs(date2), 'minutes');\n\nexport const checkNewDay = (date1: string, date2: string): boolean => !dayjs(date1).isSame(dayjs(date2), 'day');\n\nexport const formatDivider = (date: string): string => dayjs(date).format('MMMM D, YYYY');\n"
  },
  {
    "path": "web/src/lib/utils/hooks/useGetCurrentChannel.ts",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { Channel } from '../../models/channel';\nimport { cKey } from '../querykeys';\n\nexport function useGetCurrentChannel(channelId: string, guildId: string): Channel | undefined {\n  const { data } = useQuery<Channel[]>([cKey, guildId]);\n  return data?.find((c) => c.id === channelId);\n}\n"
  },
  {
    "path": "web/src/lib/utils/hooks/useGetCurrentDM.ts",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { dmKey } from '../querykeys';\nimport { DMChannel } from '../../models/dm';\n\nexport function useGetCurrentDM(channelId: string): DMChannel | undefined {\n  const { data } = useQuery<DMChannel[]>([dmKey]);\n  return data?.find((c) => c.id === channelId);\n}\n"
  },
  {
    "path": "web/src/lib/utils/hooks/useGetCurrentGuild.ts",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { gKey } from '../querykeys';\nimport { Guild } from '../../models/guild';\n\nexport function useGetCurrentGuild(guildId: string): Guild | undefined {\n  const { data: guildData } = useQuery<Guild[]>([gKey]);\n  return guildData?.find((g) => g.id === guildId);\n}\n"
  },
  {
    "path": "web/src/lib/utils/hooks/useGetFriend.ts",
    "content": "import { useQuery } from '@tanstack/react-query';\nimport { fKey } from '../querykeys';\nimport { Friend } from '../../models/friend';\n\nexport function useGetFriend(id: string): Friend | undefined {\n  const { data } = useQuery<Friend[]>([fKey]);\n  return data?.find((f) => f.id === id);\n}\n"
  },
  {
    "path": "web/src/lib/utils/hooks/useVoiceChat.ts",
    "content": "/* eslint-disable react-hooks/exhaustive-deps */\n\nimport { useEffect } from 'react';\nimport { getSameSocket } from '../../api/getSocket';\nimport { VoiceSignal } from '../../models/voice';\nimport { userStore } from '../../stores/userStore';\nimport { voiceStore } from '../../stores/voiceStore';\n\nexport function useSetupVoiceChat(guildId: string): void {\n  const socket = getSameSocket();\n\n  const current = userStore((state) => state.current);\n\n  const voiceJoinUserId = voiceStore((state) => state.voiceJoinUserId);\n  const [voiceLeaveUserId, setVoiceLeaveUserId] = voiceStore((state) => [\n    state.voiceLeaveUserId,\n    state.setVoiceLeaveUserId,\n  ]);\n\n  const [voiceClients, setVoiceClients] = voiceStore((state) => [state.voiceClients, state.setVoiceClients]);\n\n  const localStream = voiceStore((state) => state.localStream);\n  const rtcSignalData = voiceStore((state) => state.rtcSignalData);\n\n  const [getConnection, setConnection, deleteConnection] = voiceStore((state) => [\n    state.getConnection,\n    state.setConnection,\n    state.deleteConnection,\n  ]);\n\n  const sendVoiceSignal = (message: VoiceSignal): void => {\n    socket.send(\n      JSON.stringify({\n        action: 'voice-signal',\n        room: guildId,\n        message: { ...message },\n      })\n    );\n  };\n\n  // On user join, add them to the connections array and bind event handlers + create offers\n  useEffect(() => {\n    const onUserJoin = async (): Promise<void> => {\n      // Iterate over client list\n      voiceClients.forEach((user) => {\n        // If the new client is not in our list\n        if (!getConnection(user.id)) {\n          // Add this new users Peer connection to our connections map\n          setConnection(\n            user.id,\n            new RTCPeerConnection({\n              iceServers: [{ urls: 'stun:stun.services.mozilla.com' }, { urls: 'stun:stun.l.google.com:19302' }],\n            })\n          );\n          // Wait for peer to generate ice candidate\n          getConnection(user.id)!.onicecandidate = (event: RTCPeerConnectionIceEvent) => {\n            if (event.candidate !== null) {\n              sendVoiceSignal({ userId: user.id, ice: event.candidate });\n            }\n          };\n\n          // Event handler for peer adding their stream\n          getConnection(user.id)!.ontrack = (event: RTCTrackEvent) => {\n            const clients = voiceClients.map((e) => {\n              if (e.id === user.id) {\n                return { ...e, stream: event.streams[0] };\n              }\n              return e;\n            });\n\n            setVoiceClients(clients);\n          };\n\n          // Adds our local audio stream to Peer\n          localStream?.getAudioTracks().forEach((track) => getConnection(user.id)!.addTrack(track, localStream!));\n        }\n      });\n\n      // Create offer to new client joining if it is not the current user\n      if (voiceJoinUserId !== current?.id) {\n        try {\n          const description = await getConnection(voiceJoinUserId)?.createOffer();\n\n          await getConnection(voiceJoinUserId)?.setLocalDescription(description);\n\n          sendVoiceSignal({ userId: voiceJoinUserId, sdp: getConnection(voiceJoinUserId)?.localDescription });\n        } catch (err) {}\n      }\n    };\n\n    if (voiceJoinUserId && voiceClients) {\n      onUserJoin();\n    }\n  }, [voiceJoinUserId, voiceClients]);\n\n  // New message from server, configure RTC sdp session objects\n  useEffect((): void => {\n    const onMessageFromServer = async (): Promise<void> => {\n      const { userId, sdp, ice } = rtcSignalData;\n      // Check it's not coming from the current user\n      if (userId !== current?.id) {\n        if (sdp) {\n          try {\n            await getConnection(userId)!.setRemoteDescription(new RTCSessionDescription(sdp));\n\n            if (sdp?.type === 'offer') {\n              const description = await getConnection(userId)!.createAnswer();\n\n              // Improve audio quality\n              description.sdp = description.sdp?.replace(\n                'useinbandfec=1',\n                'useinbandfec=1; stereo=1; maxaveragebitrate=510000'\n              );\n\n              await getConnection(userId)!.setLocalDescription(description);\n\n              sendVoiceSignal({ userId, sdp: getConnection(userId)!.localDescription });\n            }\n          } catch (err) {}\n        }\n\n        if (ice) {\n          try {\n            await getConnection(userId)!.addIceCandidate(new RTCIceCandidate(ice));\n          } catch (err) {}\n        }\n      }\n    };\n\n    if (voiceJoinUserId !== '') {\n      onMessageFromServer();\n    }\n  }, [rtcSignalData, voiceJoinUserId]);\n\n  // If a user leaves, close the peer connection and remove them from the client list\n  useEffect((): void => {\n    if (voiceLeaveUserId !== '') {\n      // Close RTC peer connection\n      getConnection(voiceLeaveUserId)?.close();\n      deleteConnection(voiceLeaveUserId);\n      // Remove the audio element from page\n      const clients = voiceClients.filter((e) => e.id !== voiceLeaveUserId);\n      setVoiceClients(clients);\n      setVoiceLeaveUserId('');\n    }\n  }, [voiceLeaveUserId]);\n}\n"
  },
  {
    "path": "web/src/lib/utils/querykeys.ts",
    "content": "export const gKey = 'guilds';\nexport const dmKey = 'dms';\nexport const aKey = 'account';\nexport const fKey = 'friends';\nexport const rKey = 'requests';\nexport const nKey = 'notification';\nexport const cKey = 'channels';\nexport const mKey = 'members';\nexport const msgKey = 'messages';\nexport const vcKey = 'voice';\n"
  },
  {
    "path": "web/src/lib/utils/theme.ts",
    "content": "import { extendTheme } from '@chakra-ui/react';\nimport { mode } from '@chakra-ui/theme-tools';\n\nconst config: any = {\n  initialColorMode: 'dark',\n};\n\nconst styles = {\n  global: (props: any) => ({\n    body: {\n      bg: mode('gray.100', '#1b1c1d')(props),\n    },\n  }),\n};\n\nconst colors = {\n  highlight: {\n    standard: '#7289da',\n    hover: '#677bc4',\n    active: '#5b6eae',\n  },\n  brandGray: {\n    accent: '#8e9297',\n    active: '#393c43',\n    light: '#36393f',\n    dark: '#303339',\n    darker: '#202225',\n    darkest: '#18191c',\n    hover: '#32353b',\n  },\n  brandGreen: '#43b581',\n  labelGray: '#72767d',\n  menuRed: '#f04747',\n  brandBorder: '#1A202C',\n  accountBar: '#292b2f',\n  memberList: '#2f3136',\n  iconColor: '#b9bbbe',\n  messageInput: '#40444b',\n};\n\nconst fonts = {\n  body: \"'Open Sans', sans-serif\",\n};\n\nconst customTheme = extendTheme({\n  colors,\n  config,\n  styles,\n  fonts,\n});\n\nexport default customTheme;\n\nexport const scrollbarCss = {\n  '&::-webkit-scrollbar': {\n    width: '8px',\n  },\n  '&::-webkit-scrollbar-track': {\n    background: '#2f3136',\n    width: '10px',\n  },\n  '&::-webkit-scrollbar-thumb': {\n    background: 'brandGray.darker',\n    borderRadius: '18px',\n  },\n};\n"
  },
  {
    "path": "web/src/lib/utils/toErrorMap.ts",
    "content": "import { FieldError } from '../models/fieldError';\n\nexport const toErrorMap = (errors: FieldError[]): Record<string, string> => {\n  const errorMap: Record<string, string> = {};\n  errors.forEach(({ field, message }) => {\n    errorMap[field.toLowerCase()] = message;\n  });\n\n  return errorMap;\n};\n"
  },
  {
    "path": "web/src/lib/utils/validation/auth.schema.ts",
    "content": "import * as yup from 'yup';\n\nexport const LoginSchema = yup.object().shape({\n  email: yup.string().required('Email is required').defined(),\n  password: yup.string().required('Password is required').defined(),\n});\n\nexport const RegisterSchema = yup.object().shape({\n  username: yup.string().min(3).max(30).trim().required('Username is required').defined(),\n  email: yup.string().email().lowercase().required('Email is required').defined(),\n  password: yup\n    .string()\n    .min(6, 'Password must be at least 6 characters long')\n    .max(150)\n    .required('Password is required')\n    .defined(),\n});\n\nexport const UserSchema = yup.object().shape({\n  email: yup.string().email().lowercase().required('Email is required').defined(),\n  username: yup.string().min(3).max(30).trim().required('Username is required').defined(),\n});\n\nexport const ResetPasswordSchema = yup.object().shape({\n  newPassword: yup\n    .string()\n    .min(6, 'Password must be at least 6 characters long')\n    .max(150)\n    .required('New Password is required')\n    .defined(),\n  confirmNewPassword: yup\n    .string()\n    .oneOf([yup.ref('newPassword'), undefined], 'Passwords do not match')\n    .required('Confirm New Password is required')\n    .defined(),\n});\n\nexport const ChangePasswordSchema = yup.object().shape({\n  currentPassword: yup.string().required('Old Password is required').defined(),\n  newPassword: yup\n    .string()\n    .min(6, 'Password must be at least 6 characters long')\n    .max(150)\n    .required('New Password is required')\n    .defined(),\n  confirmNewPassword: yup\n    .string()\n    .oneOf([yup.ref('newPassword'), undefined], 'Passwords do not match')\n    .required('Confirm New Password is required')\n    .defined(),\n});\n\nexport const ForgotPasswordSchema = yup.object().shape({\n  email: yup.string().email().lowercase().required('Email is required').defined(),\n});\n"
  },
  {
    "path": "web/src/lib/utils/validation/channel.schema.ts",
    "content": "import * as yup from 'yup';\n\nexport const ChannelSchema = yup.object().shape({\n  name: yup.string().min(3).max(30).required('This field is required'),\n  isPublic: yup.boolean().optional().default(true),\n  members: yup.array(yup.string().optional().max(20, 'Must provide memberIds')).optional(),\n});\n"
  },
  {
    "path": "web/src/lib/utils/validation/guild.schema.ts",
    "content": "import * as yup from 'yup';\n\nexport const GuildSchema = yup.object().shape({\n  name: yup.string().min(3).max(30).required(),\n});\n"
  },
  {
    "path": "web/src/lib/utils/validation/member.schema.ts",
    "content": "import * as yup from 'yup';\n\nexport const MemberSchema = yup.object().shape({\n  nickname: yup.string().nullable().min(3).max(30),\n  color: yup\n    .string()\n    .nullable()\n    .matches(/^#[0-9a-f]{3}(?:[0-9a-f]{3})?$/i, 'The color must be a valid hex color'),\n});\n"
  },
  {
    "path": "web/src/lib/utils/validation/message.schema.ts",
    "content": "import * as yup from 'yup';\n\nconst SUPPORTED_FORMATS = ['image/jpg', 'image/jpeg', 'audio/mp3', 'audio/mpeg', 'image/png'];\n\nexport const FileSchema = yup.object().shape({\n  file: yup\n    .mixed<FileList>()\n    .nullable()\n    .test('count', 'Only one file is allowed', (value) => value?.length === 1)\n    .test('fileSize', 'The file is too large', (value) => !!value && value[0].size < 5000000)\n    .test(\n      'type',\n      'Only the following formats are accepted: Image and Audio',\n      (value) => !!value && SUPPORTED_FORMATS.includes(value[0].type)\n    ),\n});\n"
  },
  {
    "path": "web/src/react-app-env.d.ts",
    "content": "/// <reference types=\"react-scripts\" />\n"
  },
  {
    "path": "web/src/routes/AuthRoute.tsx",
    "content": "/* eslint-disable react/jsx-props-no-spreading */\nimport React from 'react';\nimport { Navigate } from 'react-router-dom';\nimport { userStore } from '../lib/stores/userStore';\n\ninterface IProps {\n  children: React.ReactNode;\n}\n\nexport const AuthRoute: React.FC<IProps> = ({ children }) => {\n  const storage = JSON.parse(localStorage.getItem('user-storage')!!);\n  const current = userStore((state) => state.current);\n\n  if (current || storage?.state?.current) {\n    return <>{children}</>;\n  }\n\n  return <Navigate to=\"/login\" />;\n};\n"
  },
  {
    "path": "web/src/routes/ForgotPassword.tsx",
    "content": "import { Box, Button, Flex, Heading, Image, useToast } from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { InputField } from '../components/common/InputField';\nimport { toErrorMap } from '../lib/utils/toErrorMap';\nimport { ForgotPasswordSchema } from '../lib/utils/validation/auth.schema';\nimport { forgotPassword } from '../lib/api/handler/auth';\n\nexport const ForgotPassword: React.FC = () => {\n  const navigate = useNavigate();\n  const toast = useToast();\n\n  return (\n    <Flex minHeight=\"100vh\" width=\"full\" align=\"center\" justifyContent=\"center\">\n      <Box px={4} width=\"full\" maxWidth=\"500px\" textAlign=\"center\">\n        <Flex mb=\"4\" justify=\"center\">\n          <Image src={`${process.env.PUBLIC_URL}/logo.png`} w=\"80px\" />\n        </Flex>\n        <Box p={4} borderRadius={4} background=\"brandGray.light\">\n          <Box textAlign=\"center\">\n            <Heading fontSize=\"24px\">Forgot Password</Heading>\n          </Box>\n          <Box my={4} textAlign=\"left\">\n            <Formik\n              initialValues={{ email: '' }}\n              validationSchema={ForgotPasswordSchema}\n              onSubmit={async (values, { setErrors }) => {\n                try {\n                  const { data } = await forgotPassword(values.email);\n                  if (data) {\n                    toast({\n                      title: 'Reset Mail.',\n                      description: 'If an account with that email already exists, we sent you an email',\n                      status: 'success',\n                      duration: 5000,\n                      isClosable: true,\n                    });\n                    navigate('/');\n                  }\n                } catch (err: any) {\n                  if (err?.response?.data?.errors) {\n                    const errors = err?.response?.data?.errors;\n                    setErrors(toErrorMap(errors));\n                  }\n                }\n              }}\n            >\n              {({ isSubmitting }) => (\n                <Form>\n                  <InputField label=\"Email\" name=\"email\" autoComplete=\"email\" type=\"email\" />\n\n                  <Button\n                    background=\"highlight.standard\"\n                    color=\"white\"\n                    width=\"full\"\n                    mt={4}\n                    type=\"submit\"\n                    isLoading={isSubmitting}\n                    _hover={{ bg: 'highlight.hover' }}\n                    _active={{ bg: 'highlight.active' }}\n                    _focus={{ boxShadow: 'none' }}\n                    fontSize=\"14px\"\n                  >\n                    Send Mail\n                  </Button>\n                </Form>\n              )}\n            </Formik>\n          </Box>\n        </Box>\n      </Box>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/routes/Home.tsx",
    "content": "import React from 'react';\nimport { useParams } from 'react-router-dom';\nimport { GuildList } from '../components/layouts/guild/GuildList';\nimport { DMSidebar } from '../components/layouts/home/DMSidebar';\nimport { FriendsDashboard } from '../components/layouts/home/dashboard/FriendsDashboard';\nimport { AppLayout } from '../components/layouts/AppLayout';\nimport { ChatScreen } from '../components/layouts/guild/chat/ChatScreen';\nimport { DMHeader } from '../components/layouts/home/DMHeader';\nimport { MessageInput } from '../components/layouts/guild/chat/MessageInput';\nimport { RouterProps } from '../lib/models/routerProps';\n\nexport const Home: React.FC = () => {\n  const { channelId } = useParams<keyof RouterProps>() as RouterProps;\n\n  return (\n    <AppLayout>\n      <GuildList />\n      <DMSidebar />\n      {channelId === undefined ? (\n        <FriendsDashboard />\n      ) : (\n        <>\n          <DMHeader />\n          <ChatScreen />\n          <MessageInput />\n        </>\n      )}\n    </AppLayout>\n  );\n};\n"
  },
  {
    "path": "web/src/routes/Invite.tsx",
    "content": "import React, { useEffect, useState } from 'react';\nimport { Link as RLink, useNavigate, useParams } from 'react-router-dom';\nimport { Box, Flex, Image, Link, Text } from '@chakra-ui/react';\nimport { joinGuild } from '../lib/api/handler/guilds';\n\ninterface InviteRouter {\n  link: string;\n}\n\nexport const Invite: React.FC = () => {\n  const { link } = useParams<keyof InviteRouter>() as InviteRouter;\n  const [errors, setErrors] = useState<string | null>(null);\n  const navigate = useNavigate();\n\n  useEffect(() => {\n    const handleJoin = async (): Promise<void> => {\n      try {\n        const { data } = await joinGuild({ link });\n        if (data) {\n          navigate(`/channels/${data.id}/${data.default_channel_id}`, { replace: true });\n        }\n      } catch (err: any) {\n        const status = err?.response?.status;\n        if (status === 400 || status === 404 || status === 500) {\n          setErrors(err?.response?.data?.error?.message);\n        }\n      }\n    };\n    handleJoin();\n  }, [link, navigate]);\n\n  return (\n    <Flex minHeight=\"100vh\" align=\"center\" justify=\"center\" h=\"full\">\n      <Box textAlign=\"center\">\n        <Flex mb=\"4\" justify=\"center\">\n          <Image src={`${process.env.PUBLIC_URL}/logo.png`} w=\"80px\" />\n        </Flex>\n        <Text>Fetching server info. Please wait.</Text>\n        <Text>You will be automatically redirected.</Text>\n        {errors && (\n          <Box>\n            <Text my=\"2\" textColor=\"menuRed\">\n              {errors}\n            </Text>\n            <Text>\n              Click{' '}\n              <Link as={RLink} to=\"/channels/me\" color=\"highlight.standard\" _focus={{ outline: 'none' }}>\n                here\n              </Link>{' '}\n              to return.\n            </Text>\n          </Box>\n        )}\n      </Box>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/routes/Landing.tsx",
    "content": "import React from 'react';\nimport { LandingLayout } from '../components/layouts/LandingLayout';\nimport { Hero } from '../components/sections/Hero';\n\nexport const Landing: React.FC = () => (\n  <LandingLayout>\n    <Hero\n      title=\"Valkyrie\"\n      subtitle=\"Whether you’re part of a school club,\n        gaming group, worldwide art community,\n        or just a handful of friends that want to spend time together,\n        Valkyrie makes it easy to talk every day and hang out more often.\"\n      image={`${process.env.PUBLIC_URL}/logo.png`}\n      ctaText=\"Get Started\"\n      ctaLink=\"/register\"\n    />\n  </LandingLayout>\n);\n"
  },
  {
    "path": "web/src/routes/Login.tsx",
    "content": "import { Box, Button, Flex, Heading, Image, Link, Text } from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React from 'react';\nimport { Link as RLink, useNavigate } from 'react-router-dom';\nimport { InputField } from '../components/common/InputField';\nimport { toErrorMap } from '../lib/utils/toErrorMap';\nimport { userStore } from '../lib/stores/userStore';\nimport { LoginSchema } from '../lib/utils/validation/auth.schema';\nimport { login } from '../lib/api/handler/auth';\n\nexport const Login: React.FC = () => {\n  const navigate = useNavigate();\n  const setUser = userStore((state) => state.setUser);\n\n  return (\n    <Flex minHeight=\"100vh\" width=\"full\" align=\"center\" justifyContent=\"center\">\n      <Box px={4} width=\"full\" maxWidth=\"500px\" textAlign=\"center\">\n        <Flex mb=\"4\" justify=\"center\">\n          <Image src={`${process.env.PUBLIC_URL}/logo.png`} w=\"80px\" />\n        </Flex>\n        <Box p={4} borderRadius={4} background=\"brandGray.light\">\n          <Box textAlign=\"center\">\n            <Heading fontSize=\"24px\">Welcome Back</Heading>\n          </Box>\n          <Box my={4} textAlign=\"left\">\n            <Formik\n              initialValues={{\n                email: '',\n                password: '',\n              }}\n              validationSchema={LoginSchema}\n              onSubmit={async (values, { setErrors }) => {\n                try {\n                  const { data } = await login(values);\n                  if (data) {\n                    setUser(data);\n                    navigate('/channels/me');\n                  }\n                } catch (err: any) {\n                  if (err?.response?.status === 401) {\n                    setErrors({ password: 'Invalid Credentials' });\n                  }\n                  if (err?.response?.data?.errors) {\n                    const errors = err?.response?.data?.errors;\n                    setErrors(toErrorMap(errors));\n                  }\n                }\n              }}\n            >\n              {({ isSubmitting }) => (\n                <Form>\n                  <InputField label=\"Email\" name=\"email\" autoComplete=\"email\" type=\"email\" />\n\n                  <InputField label=\"password\" name=\"password\" autoComplete=\"password\" type=\"password\" />\n\n                  <Box mt={4}>\n                    <Link as={RLink} to=\"/forgot-password\" textColor=\"highlight.standard\" _focus={{ outline: 'none' }}>\n                      Forgot Password?\n                    </Link>\n                  </Box>\n\n                  <Button\n                    background=\"highlight.standard\"\n                    color=\"white\"\n                    width=\"full\"\n                    mt={4}\n                    type=\"submit\"\n                    isLoading={isSubmitting}\n                    _hover={{ bg: 'highlight.hover' }}\n                    _active={{ bg: 'highlight.active' }}\n                    _focus={{ boxShadow: 'none' }}\n                  >\n                    Login\n                  </Button>\n                  <Text mt=\"4\">\n                    Don&apos;t have an account yet?{' '}\n                    <Link as={RLink} to=\"/register\" textColor=\"highlight.standard\" _focus={{ outline: 'none' }}>\n                      Sign Up\n                    </Link>\n                  </Text>\n                </Form>\n              )}\n            </Formik>\n          </Box>\n        </Box>\n      </Box>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/routes/Register.tsx",
    "content": "import React, { useState } from 'react';\nimport { Box, Button, Flex, Heading, Image, Link, Text } from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport { Link as RLink, useNavigate } from 'react-router-dom';\nimport { InputField } from '../components/common/InputField';\nimport { toErrorMap } from '../lib/utils/toErrorMap';\nimport { userStore } from '../lib/stores/userStore';\nimport { RegisterSchema } from '../lib/utils/validation/auth.schema';\nimport { register } from '../lib/api/handler/auth';\n\nexport const Register: React.FC = () => {\n  const navigate = useNavigate();\n  const setUser = userStore((state) => state.setUser);\n  const [error, showError] = useState(false);\n\n  return (\n    <Flex minHeight=\"100vh\" width=\"full\" align=\"center\" justifyContent=\"center\">\n      <Box px={4} width=\"full\" maxWidth=\"500px\" textAlign=\"center\">\n        <Flex mb=\"4\" justify=\"center\">\n          <Image src={`${process.env.PUBLIC_URL}/logo.png`} w=\"80px\" />\n        </Flex>\n        <Box p={4} borderRadius={4} background=\"brandGray.light\">\n          <Box textAlign=\"center\">\n            <Heading fontSize=\"24px\">Welcome to Valkyrie</Heading>\n          </Box>\n          <Box my={4} textAlign=\"left\">\n            <Formik\n              initialValues={{\n                email: '',\n                username: '',\n                password: '',\n              }}\n              validationSchema={RegisterSchema}\n              onSubmit={async (values, { setErrors }) => {\n                try {\n                  const { data } = await register(values);\n                  if (data) {\n                    setUser(data);\n                    navigate('/channels/me');\n                  }\n                } catch (err: any) {\n                  if (err?.response?.status === 500) {\n                    showError(true);\n                  }\n                  if (err?.response?.data?.errors) {\n                    const errors = err?.response?.data?.errors;\n                    setErrors(toErrorMap(errors));\n                  }\n                }\n              }}\n            >\n              {({ isSubmitting }) => (\n                <Form>\n                  <InputField label=\"Email\" name=\"email\" autoComplete=\"email\" type=\"email\" />\n\n                  <InputField label=\"username\" name=\"username\" />\n\n                  <InputField label=\"password\" name=\"password\" autoComplete=\"password\" type=\"password\" />\n\n                  <Button\n                    background=\"highlight.standard\"\n                    color=\"white\"\n                    width=\"full\"\n                    mt={4}\n                    type=\"submit\"\n                    isLoading={isSubmitting}\n                    _hover={{ bg: 'highlight.hover' }}\n                    _active={{ bg: 'highlight.active' }}\n                    _focus={{ boxShadow: 'none' }}\n                  >\n                    Register\n                  </Button>\n                  {error && (\n                    <Text mt=\"4\" color=\"menuRed\" align=\"center\">\n                      Server Error. Try again later\n                    </Text>\n                  )}\n                  <Text mt=\"4\">\n                    Already have an account?{' '}\n                    <Link as={RLink} to=\"/login\" textColor=\"highlight.standard\" _focus={{ outline: 'none' }}>\n                      Sign In\n                    </Link>\n                  </Text>\n                </Form>\n              )}\n            </Formik>\n          </Box>\n        </Box>\n      </Box>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/routes/ResetPassword.tsx",
    "content": "import { Box, Button, Flex, Heading, Image, Link, Text } from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React, { useState } from 'react';\nimport { Link as RLink, useNavigate, useParams } from 'react-router-dom';\nimport { InputField } from '../components/common/InputField';\nimport { toErrorMap } from '../lib/utils/toErrorMap';\nimport { userStore } from '../lib/stores/userStore';\nimport { ResetPasswordSchema } from '../lib/utils/validation/auth.schema';\nimport { resetPassword } from '../lib/api/handler/auth';\n\ntype TokenProps = {\n  token: string;\n};\n\nexport const ResetPassword: React.FC = () => {\n  const navigate = useNavigate();\n  const { token } = useParams<keyof TokenProps>() as TokenProps;\n  const [showError, setShowError] = useState(false);\n  const [tokenError, setTokenError] = useState('');\n  const setUser = userStore((state) => state.setUser);\n\n  return (\n    <Flex minHeight=\"100vh\" width=\"full\" align=\"center\" justifyContent=\"center\">\n      <Box px={4} width=\"full\" maxWidth=\"500px\" textAlign=\"center\">\n        <Flex mb=\"4\" justify=\"center\">\n          <Image src={`${process.env.PUBLIC_URL}/logo.png`} w=\"80px\" />\n        </Flex>\n        <Box p={4} borderRadius={4} background=\"brandGray.light\">\n          <Box textAlign=\"center\">\n            <Heading fontSize=\"24px\">Reset Password</Heading>\n          </Box>\n          <Box my={4} textAlign=\"left\">\n            <Formik\n              initialValues={{\n                newPassword: '',\n                confirmNewPassword: '',\n              }}\n              validationSchema={ResetPasswordSchema}\n              onSubmit={async (values, { setErrors }) => {\n                try {\n                  const { data } = await resetPassword({\n                    ...values,\n                    token,\n                  });\n                  if (data) {\n                    setUser(data);\n                    navigate('/channels/me');\n                  }\n                } catch (err: any) {\n                  if (err?.response?.status === 500) {\n                    setShowError(true);\n                  } else {\n                    const errors = err?.response?.data?.errors;\n                    const errorMap = toErrorMap(errors);\n\n                    if ('token' in errorMap) {\n                      setTokenError(errorMap.token);\n                    }\n                    setErrors(errorMap);\n                  }\n                }\n              }}\n            >\n              {({ isSubmitting }) => (\n                <Form>\n                  <InputField label=\"New Password\" name=\"newPassword\" autoComplete=\"new-password\" type=\"password\" />\n\n                  <InputField label=\"Confirm New Password\" name=\"confirmNewPassword\" type=\"password\" />\n\n                  <Button\n                    background=\"highlight.standard\"\n                    color=\"white\"\n                    width=\"full\"\n                    mt={4}\n                    type=\"submit\"\n                    isLoading={isSubmitting}\n                    _hover={{ bg: 'highlight.hover' }}\n                    _active={{ bg: 'highlight.active' }}\n                    _focus={{ boxShadow: 'none' }}\n                  >\n                    Reset Password\n                  </Button>\n                </Form>\n              )}\n            </Formik>\n            {showError && (\n              <Text mt=\"4\" color=\"menuRed\" align=\"center\">\n                Server Error. Try again later\n              </Text>\n            )}\n            {tokenError && (\n              <Flex direction=\"column\" mt=\"4\" justify=\"center\" align=\"center\">\n                <Text color=\"menuRed\">Invalid or expired token.</Text>\n                <Link as={RLink} to=\"/forgot-password\" _focus={{ outline: 'none' }}>\n                  Click here to get a new token\n                </Link>\n              </Flex>\n            )}\n          </Box>\n        </Box>\n      </Box>\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/routes/Routes.tsx",
    "content": "import React from 'react';\nimport { BrowserRouter, Route, Routes } from 'react-router-dom';\nimport { Login } from './Login';\nimport { Register } from './Register';\nimport { ForgotPassword } from './ForgotPassword';\nimport { ResetPassword } from './ResetPassword';\nimport { Home } from './Home';\nimport { ViewGuild } from './ViewGuild';\nimport { AuthRoute } from './AuthRoute';\nimport { Settings } from './Settings';\nimport { Landing } from './Landing';\nimport { Invite } from './Invite';\n\nexport const AppRoutes: React.FC = () => (\n  <BrowserRouter>\n    <Routes>\n      <Route path=\"/login\" element={<Login />} />\n      <Route path=\"/register\" element={<Register />} />\n      <Route path=\"/forgot-password\" element={<ForgotPassword />} />\n      <Route path=\"/reset-password/:token\" element={<ResetPassword />} />\n      <Route path=\"/\" element={<Landing />} />\n      <Route\n        path=\"/channels/me\"\n        element={\n          <AuthRoute>\n            <Home />\n          </AuthRoute>\n        }\n      />\n      <Route\n        path=\"/channels/me/:channelId\"\n        element={\n          <AuthRoute>\n            <Home />\n          </AuthRoute>\n        }\n      />\n      <Route\n        path=\"/channels/:guildId/:channelId\"\n        element={\n          <AuthRoute>\n            <ViewGuild />\n          </AuthRoute>\n        }\n      />\n      <Route\n        path=\"/account\"\n        element={\n          <AuthRoute>\n            <Settings />\n          </AuthRoute>\n        }\n      />\n      <Route\n        path=\"/:link\"\n        element={\n          <AuthRoute>\n            <Invite />\n          </AuthRoute>\n        }\n      />\n    </Routes>\n  </BrowserRouter>\n);\n"
  },
  {
    "path": "web/src/routes/Settings.tsx",
    "content": "import {\n  Avatar,\n  Box,\n  Button,\n  Divider,\n  Flex,\n  Heading,\n  LightMode,\n  Spacer,\n  Tooltip,\n  useDisclosure,\n  useToast,\n} from '@chakra-ui/react';\nimport { Form, Formik } from 'formik';\nimport React, { useRef, useState } from 'react';\nimport { useQuery, useQueryClient } from '@tanstack/react-query';\nimport { useNavigate } from 'react-router-dom';\nimport { InputField } from '../components/common/InputField';\nimport { ChangePasswordModal } from '../components/modals/ChangePasswordModal';\nimport { toErrorMap } from '../lib/utils/toErrorMap';\nimport { userStore } from '../lib/stores/userStore';\nimport { UserSchema } from '../lib/utils/validation/auth.schema';\nimport { getAccount, updateAccount } from '../lib/api/handler/account';\nimport { logout } from '../lib/api/handler/auth';\nimport { CropImageModal } from '../components/modals/CropImageModal';\nimport { aKey } from '../lib/utils/querykeys';\nimport { Account } from '../lib/models/account';\n\nexport const Settings: React.FC = () => {\n  const navigate = useNavigate();\n  const toast = useToast();\n  const { isOpen, onOpen, onClose } = useDisclosure();\n  const { isOpen: cropperIsOpen, onOpen: cropperOnOpen, onClose: cropperOnClose } = useDisclosure();\n\n  const { data: user } = useQuery<Account>([aKey], () => getAccount().then((response) => response.data));\n  const cache = useQueryClient();\n\n  const logoutUser = userStore((state) => state.logout);\n  const setUser = userStore((state) => state.setUser);\n\n  const inputFile: any = useRef(null);\n  const [imageUrl, setImageUrl] = useState<string | null>(user?.image ?? null);\n  const [cropImage, setCropImage] = useState('');\n  const [croppedImage, setCroppedImage] = useState<File | null>(null);\n\n  const closeClicked = (): void => {\n    navigate(-1);\n  };\n\n  const applyCrop = (file: Blob): void => {\n    setImageUrl(URL.createObjectURL(file));\n    setCroppedImage(new File([file], 'avatar', { type: 'image/jpeg' }));\n    cropperOnClose();\n  };\n\n  const logoutClicked = async (): Promise<void> => {\n    const { data } = await logout();\n    if (data) {\n      cache.clear();\n      logoutUser();\n      navigate('/', { replace: true });\n    }\n  };\n\n  if (!user) return null;\n\n  return (\n    <Flex minHeight=\"100vh\" width=\"full\" align=\"center\" justifyContent=\"center\">\n      <Box px={4} width=\"full\" maxWidth=\"500px\">\n        <Flex mb=\"4\" justify=\"center\">\n          <Heading fontSize=\"24px\">MY ACCOUNT</Heading>\n        </Flex>\n        <Box p={4} borderRadius={4} background=\"brandGray.light\">\n          <Box>\n            <Formik\n              initialValues={{\n                email: user.email,\n                username: user.username,\n                image: null,\n              }}\n              validationSchema={UserSchema}\n              onSubmit={async (values, { setErrors }) => {\n                try {\n                  const formData = new FormData();\n                  formData.append('email', values.email);\n                  formData.append('username', values.username);\n\n                  if (croppedImage) {\n                    formData.append('image', croppedImage);\n                  }\n                  const { data } = await updateAccount(formData);\n                  if (data) {\n                    setUser(data);\n                    toast({\n                      title: 'Account Updated.',\n                      status: 'success',\n                      duration: 3000,\n                      isClosable: true,\n                    });\n                  }\n                } catch (err: any) {\n                  if (err?.response?.status === 500) {\n                    toast({\n                      title: 'Server Error',\n                      description: 'Try again later',\n                      status: 'error',\n                      duration: 3000,\n                      isClosable: true,\n                    });\n                  }\n                  if (err?.response?.data?.errors) {\n                    const errors = err?.response?.data?.errors;\n                    setErrors(toErrorMap(errors));\n                  }\n                }\n              }}\n            >\n              {({ isSubmitting, values }) => (\n                <Form>\n                  <Flex mb=\"4\" justify=\"center\">\n                    <Tooltip label=\"Change Avatar\" aria-label=\"Change Avatar\">\n                      <Avatar\n                        size=\"xl\"\n                        name={user?.username}\n                        src={imageUrl || user?.image}\n                        _hover={{\n                          cursor: 'pointer',\n                          opacity: 0.5,\n                        }}\n                        onClick={() => inputFile.current.click()}\n                      />\n                    </Tooltip>\n                    <input\n                      type=\"file\"\n                      name=\"image\"\n                      accept=\"image/*\"\n                      ref={inputFile}\n                      hidden\n                      onChange={async (e) => {\n                        if (!e.currentTarget.files) return;\n                        setCropImage(URL.createObjectURL(e.currentTarget.files[0]));\n                        cropperOnOpen();\n                      }}\n                    />\n                  </Flex>\n                  <Box my={4}>\n                    <InputField\n                      value={values.email}\n                      type=\"email\"\n                      placeholder=\"Email\"\n                      label=\"Email\"\n                      name=\"email\"\n                      autoComplete=\"email\"\n                    />\n\n                    <InputField\n                      value={values.username}\n                      placeholder=\"Username\"\n                      label=\"Username\"\n                      name=\"username\"\n                      autoComplete=\"username\"\n                    />\n\n                    <Flex my={8} align=\"end\">\n                      <Spacer />\n                      <Button mr={4} colorScheme=\"white\" variant=\"outline\" onClick={closeClicked} fontSize=\"14px\">\n                        Close\n                      </Button>\n\n                      <LightMode>\n                        <Button type=\"submit\" colorScheme=\"green\" isLoading={isSubmitting} fontSize=\"14px\">\n                          Update\n                        </Button>\n                      </LightMode>\n                    </Flex>\n                  </Box>\n                </Form>\n              )}\n            </Formik>\n          </Box>\n          <Divider my=\"4\" />\n          <Flex>\n            <Heading fontSize=\"18px\">PASSWORD AND AUTHENTICATION</Heading>\n          </Flex>\n          <Flex mt=\"4\">\n            <Button\n              background=\"highlight.standard\"\n              color=\"white\"\n              _hover={{ bg: 'highlight.hover' }}\n              _active={{ bg: 'highlight.active' }}\n              _focus={{ boxShadow: 'none' }}\n              onClick={onOpen}\n              fontSize=\"14px\"\n            >\n              Change Password\n            </Button>\n\n            <Spacer />\n            <Button colorScheme=\"red\" variant=\"outline\" onClick={logoutClicked} fontSize=\"14px\">\n              Logout\n            </Button>\n          </Flex>\n        </Box>\n      </Box>\n      {isOpen && <ChangePasswordModal isOpen={isOpen} onClose={onClose} />}\n      {cropperIsOpen && (\n        <CropImageModal\n          isOpen={cropperIsOpen}\n          onClose={cropperOnClose}\n          initialImage={cropImage}\n          applyCrop={applyCrop}\n        />\n      )}\n    </Flex>\n  );\n};\n"
  },
  {
    "path": "web/src/routes/ViewGuild.tsx",
    "content": "import React from 'react';\nimport { Channels } from '../components/layouts/guild/Channels';\nimport { GuildList } from '../components/layouts/guild/GuildList';\nimport { ChannelHeader } from '../components/layouts/guild/ChannelHeader';\nimport { MemberList } from '../components/layouts/guild/MemberList';\nimport { MessageInput } from '../components/layouts/guild/chat/MessageInput';\nimport { ChatScreen } from '../components/layouts/guild/chat/ChatScreen';\nimport { AppLayout } from '../components/layouts/AppLayout';\nimport { settingsStore } from '../lib/stores/settingsStore';\n\nexport const ViewGuild: React.FC = () => {\n  const showMemberList = settingsStore((state) => state.showMembers);\n\n  return (\n    <AppLayout showLastColumn={showMemberList}>\n      <GuildList />\n      <Channels />\n      <ChannelHeader />\n      <ChatScreen />\n      <MessageInput />\n      {showMemberList && <MemberList />}\n    </AppLayout>\n  );\n};\n"
  },
  {
    "path": "web/src/setupTests.ts",
    "content": "import '@testing-library/jest-dom';\nimport { setupServer } from 'msw/node';\nimport { handlers } from './tests/testUtils';\n\nexport const server = setupServer(...handlers);\n\n// Establish API mocking before all tests.\nbeforeAll(() => server.listen());\n// Reset any request handlers that we may add during the tests,\n// so they don't affect other tests.\nafterEach(() => server.resetHandlers());\n// Clean up after the tests are finished.\nafterAll(() => server.close());\n"
  },
  {
    "path": "web/src/tests/fixture/accountFixture.ts",
    "content": "import { Account } from '../../lib/models/account';\n\nexport const mockAccount: Account = {\n  id: '1444337838748340224',\n  username: 'Sen',\n  email: 'sen@example.com',\n  image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n};\n"
  },
  {
    "path": "web/src/tests/fixture/channelFixtures.ts",
    "content": "import { Channel } from '../../lib/models/channel';\n\nexport const mockChannel: Channel = {\n  id: '1444930025999568896',\n  name: 'general',\n  isPublic: true,\n  hasNotification: false,\n};\n\nexport const mockChannelList = [mockChannel];\n"
  },
  {
    "path": "web/src/tests/fixture/dmFixtures.ts",
    "content": "import { DMChannel } from '../../lib/models/dm';\n\nexport const mockDMChannel: DMChannel = {\n  id: '1446384585456750592',\n  user: {\n    id: '1446384528997224448',\n    username: 'Alice',\n    image: 'https://gravatar.com/avatar/c160f8cc69a4f0bf2b0362752353d060?d=identicon',\n    isOnline: false,\n    isFriend: false,\n  },\n};\n\nexport const mockDMChannelList = [mockDMChannel];\n"
  },
  {
    "path": "web/src/tests/fixture/friendFixture.ts",
    "content": "import { Friend } from '../../lib/models/friend';\n\nexport const mockFriend: Friend = {\n  id: '1446384528997224448',\n  username: 'Alice',\n  image: 'https://gravatar.com/avatar/c160f8cc69a4f0bf2b0362752353d060?d=identicon',\n  isOnline: false,\n};\n\nexport const mockFriendList = [mockFriend];\n"
  },
  {
    "path": "web/src/tests/fixture/guildFixtures.ts",
    "content": "import { Guild } from '../../lib/models/guild';\n\nexport const mockGuild: Guild = {\n  id: '1444930025953431552',\n  name: \"Sen's server\",\n  ownerId: '1444337838748340224',\n  icon: undefined,\n  hasNotification: false,\n  default_channel_id: '1444930025999568896',\n};\n\nexport const mockGuildList = [mockGuild];\n"
  },
  {
    "path": "web/src/tests/fixture/memberFixtures.ts",
    "content": "import { Member } from '../../lib/models/member';\n\nexport const mockMember: Member = {\n  id: '1444337838748340224',\n  username: 'Sen',\n  image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n  isOnline: false,\n  nickname: null,\n  color: null,\n  isFriend: false,\n};\n\nexport const mockMemberList = [mockMember];\n"
  },
  {
    "path": "web/src/tests/fixture/messageFixtures.ts",
    "content": "import { Message } from '../../lib/models/message';\n\nexport const mockMessage: Message = {\n  id: '1444930038456651776',\n  text: 'Hello World',\n  createdAt: '2021-10-04T07:39:01.32804Z',\n  updatedAt: '2021-10-04T07:39:01.32804Z',\n  attachment: undefined,\n  user: {\n    id: '1444337838748340224',\n    username: 'Sen',\n    image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n    isOnline: false,\n    nickname: undefined,\n    color: undefined,\n    isFriend: false,\n  },\n};\n\nexport const mockMessageList: Message[] = [\n  {\n    id: '1446470552582623232',\n    text: '40',\n    createdAt: '2021-10-08T13:40:28.517656Z',\n    updatedAt: '2021-10-08T13:40:28.517656Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470544797995008',\n    text: '39',\n    createdAt: '2021-10-08T13:40:26.661389Z',\n    updatedAt: '2021-10-08T13:40:26.661389Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470535625052160',\n    text: '38',\n    createdAt: '2021-10-08T13:40:24.474183Z',\n    updatedAt: '2021-10-08T13:40:24.474183Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470519615393792',\n    text: '37',\n    createdAt: '2021-10-08T13:40:20.657062Z',\n    updatedAt: '2021-10-08T13:40:20.657062Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470497800818688',\n    text: '36',\n    createdAt: '2021-10-08T13:40:15.455995Z',\n    updatedAt: '2021-10-08T13:40:15.455995Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470480964882432',\n    text: '35',\n    createdAt: '2021-10-08T13:40:11.442021Z',\n    updatedAt: '2021-10-08T13:40:11.442021Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470477739462656',\n    text: '34',\n    createdAt: '2021-10-08T13:40:10.673121Z',\n    updatedAt: '2021-10-08T13:40:10.673121Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470472286867456',\n    text: '33',\n    createdAt: '2021-10-08T13:40:09.37347Z',\n    updatedAt: '2021-10-08T13:40:09.37347Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470469535404032',\n    text: '32',\n    createdAt: '2021-10-08T13:40:08.717298Z',\n    updatedAt: '2021-10-08T13:40:08.717298Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470463248142336',\n    text: '31',\n    createdAt: '2021-10-08T13:40:07.218367Z',\n    updatedAt: '2021-10-08T13:40:07.218367Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470454104559616',\n    text: '30',\n    createdAt: '2021-10-08T13:40:05.038445Z',\n    updatedAt: '2021-10-08T13:40:05.038445Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470449973170176',\n    text: '29',\n    createdAt: '2021-10-08T13:40:04.053672Z',\n    updatedAt: '2021-10-08T13:40:04.053672Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470443010625536',\n    text: '28',\n    createdAt: '2021-10-08T13:40:02.393155Z',\n    updatedAt: '2021-10-08T13:40:02.393155Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470436186492928',\n    text: '27',\n    createdAt: '2021-10-08T13:40:00.766696Z',\n    updatedAt: '2021-10-08T13:40:00.766696Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470429337194496',\n    text: '26',\n    createdAt: '2021-10-08T13:39:59.133462Z',\n    updatedAt: '2021-10-08T13:39:59.133462Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470426405376000',\n    text: '25',\n    createdAt: '2021-10-08T13:39:58.433884Z',\n    updatedAt: '2021-10-08T13:39:58.433884Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470419178590208',\n    text: '24',\n    createdAt: '2021-10-08T13:39:56.711199Z',\n    updatedAt: '2021-10-08T13:39:56.711199Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470417224044544',\n    text: '23',\n    createdAt: '2021-10-08T13:39:56.245516Z',\n    updatedAt: '2021-10-08T13:39:56.245516Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470414753599488',\n    text: '22',\n    createdAt: '2021-10-08T13:39:55.656156Z',\n    updatedAt: '2021-10-08T13:39:55.656156Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470411712729088',\n    text: '21',\n    createdAt: '2021-10-08T13:39:54.93168Z',\n    updatedAt: '2021-10-08T13:39:54.93168Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470408772521984',\n    text: '20',\n    createdAt: '2021-10-08T13:39:54.230331Z',\n    updatedAt: '2021-10-08T13:39:54.230331Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470405018619904',\n    text: '19',\n    createdAt: '2021-10-08T13:39:53.335287Z',\n    updatedAt: '2021-10-08T13:39:53.335287Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470400451022848',\n    text: '18',\n    createdAt: '2021-10-08T13:39:52.246774Z',\n    updatedAt: '2021-10-08T13:39:52.246774Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470395317194752',\n    text: '17',\n    createdAt: '2021-10-08T13:39:51.022541Z',\n    updatedAt: '2021-10-08T13:39:51.022541Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470388576948224',\n    text: '16',\n    createdAt: '2021-10-08T13:39:49.415463Z',\n    updatedAt: '2021-10-08T13:39:49.415463Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470385527689216',\n    text: '15',\n    createdAt: '2021-10-08T13:39:48.688824Z',\n    updatedAt: '2021-10-08T13:39:48.688824Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470383225016320',\n    text: '14',\n    createdAt: '2021-10-08T13:39:48.139847Z',\n    updatedAt: '2021-10-08T13:39:48.139847Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470380217700352',\n    text: '13',\n    createdAt: '2021-10-08T13:39:47.422735Z',\n    updatedAt: '2021-10-08T13:39:47.422735Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470374060462080',\n    text: '12',\n    createdAt: '2021-10-08T13:39:45.954377Z',\n    updatedAt: '2021-10-08T13:39:45.954377Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470370101039104',\n    text: '11',\n    createdAt: '2021-10-08T13:39:45.010823Z',\n    updatedAt: '2021-10-08T13:39:45.010823Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470366200336384',\n    text: '10',\n    createdAt: '2021-10-08T13:39:44.079948Z',\n    updatedAt: '2021-10-08T13:39:44.079948Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470357572653056',\n    text: '9',\n    createdAt: '2021-10-08T13:39:42.022909Z',\n    updatedAt: '2021-10-08T13:39:42.022909Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470356079480832',\n    text: '8',\n    createdAt: '2021-10-08T13:39:41.667508Z',\n    updatedAt: '2021-10-08T13:39:41.667508Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470354418536448',\n    text: '7',\n    createdAt: '2021-10-08T13:39:41.271009Z',\n    updatedAt: '2021-10-08T13:39:41.271009Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n  {\n    id: '1446470352786952192',\n    text: '6',\n    createdAt: '2021-10-08T13:39:40.882862Z',\n    updatedAt: '2021-10-08T13:39:40.882862Z',\n    attachment: undefined,\n    user: {\n      id: '1444337838748340224',\n      username: 'Sen',\n      image: 'https://gravatar.com/avatar/b8abd16496a669abb69ad275a0b0103c?d=identicon',\n      isOnline: false,\n      nickname: undefined,\n      color: undefined,\n      isFriend: false,\n    },\n  },\n];\n"
  },
  {
    "path": "web/src/tests/fixture/requestFixtures.ts",
    "content": "import { FriendRequest } from '../../lib/models/friend';\n\nexport const mockRequest: FriendRequest = {\n  id: '1446429666675003392',\n  username: 'John',\n  image: 'https://gravatar.com/avatar/d4c74594d841139328695756648b6bd6?d=identicon',\n  type: 0,\n};\n\nexport const mockRequestList = [mockRequest];\n"
  },
  {
    "path": "web/src/tests/queries/account.test.tsx",
    "content": "import { renderHook } from '@testing-library/react-hooks';\nimport { useQuery } from '@tanstack/react-query';\nimport { rest } from 'msw';\nimport { aKey } from '../../lib/utils/querykeys';\nimport { createQueryClientWrapper } from '../testUtils';\nimport { server } from '../../setupTests';\nimport { getAccount } from '../../lib/api/handler/account';\nimport { mockAccount } from '../fixture/accountFixture';\n\ndescribe('useQuery - getAccount', () => {\n  it(\"successfully fetches the user's info\", async () => {\n    const { result, waitForNextUpdate } = renderHook(\n      () =>\n        useQuery([aKey], async () => {\n          const { data } = await getAccount();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    expect(result.current.isFetching).toBe(true);\n    await waitForNextUpdate();\n\n    expect(result.current.isFetching).toBe(false);\n    expect(result.current.isSuccess).toBe(true);\n\n    expect(result.current.data).not.toBeNull();\n    expect(result.current.data).not.toBeUndefined();\n\n    const account = result.current.data;\n    expect(account).toEqual(mockAccount);\n    expect(account?.id).toBeDefined();\n    expect(account?.username).toBeDefined();\n    expect(account?.email).toBeDefined();\n    expect(account?.image).toBeDefined();\n  });\n\n  it('returns an error when the server returns status 500', async () => {\n    server.use(rest.get('*', (req, res, ctx) => res(ctx.status(500))));\n\n    const { result, waitFor } = renderHook(\n      () =>\n        useQuery([aKey], async () => {\n          const { data } = await getAccount();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    await waitFor(() => result.current.isError);\n\n    expect(result.current.error).toBeDefined();\n    expect(result.current.data).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "web/src/tests/queries/channel.test.tsx",
    "content": "import { renderHook } from '@testing-library/react-hooks';\nimport { QueryClientProvider, useQuery } from '@tanstack/react-query';\nimport { rest } from 'msw';\nimport * as React from 'react';\nimport { createQueryClientWrapper, createTestQueryClientWithData, IQueryWrapperProps } from '../testUtils';\nimport { cKey } from '../../lib/utils/querykeys';\nimport { server } from '../../setupTests';\nimport { mockGuild } from '../fixture/guildFixtures';\nimport { getChannels } from '../../lib/api/handler/channel';\nimport { mockChannel, mockChannelList } from '../fixture/channelFixtures';\nimport { useGetCurrentChannel } from '../../lib/utils/hooks/useGetCurrentChannel';\nimport { Channel } from '../../lib/models/channel';\n\ndescribe('useQuery - getChannels', () => {\n  it(\"successfully fetches the current guild's channel list\", async () => {\n    const guildId = '12312456127277383';\n\n    const { result, waitForNextUpdate } = renderHook(\n      () =>\n        useQuery([cKey, guildId], async () => {\n          const { data } = await getChannels(guildId);\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    expect(result.current.isFetching).toBe(true);\n    await waitForNextUpdate();\n\n    expect(result.current.isFetching).toBe(false);\n    expect(result.current.isSuccess).toBe(true);\n\n    expect(result.current.data).not.toBeNull();\n    expect(result.current.data).not.toBeUndefined();\n    expect(result.current.data?.length).toEqual(1);\n\n    const channel = result.current.data?.[0];\n    expect(channel).toEqual(mockChannel);\n    expect(channel?.id).toBeDefined();\n    expect(channel?.name).toBeDefined();\n    expect(channel?.isPublic).toBeDefined();\n    expect(channel?.hasNotification).toBeFalsy();\n  });\n\n  it('returns an error when the server returns status 500', async () => {\n    const guildId = '12312456127277383';\n    server.use(rest.get('*', (req, res, ctx) => res(ctx.status(500))));\n\n    const { result, waitFor } = renderHook(\n      () =>\n        useQuery([cKey, guildId], async () => {\n          const { data } = await getChannels(guildId);\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    await waitFor(() => result.current.isError);\n\n    expect(result.current.error).toBeDefined();\n    expect(result.current.data).toBeUndefined();\n  });\n});\n\ndescribe('useGetCurrentChannel', () => {\n  it('successfully fetches the channel for the given ID and key', async () => {\n    const channelId = mockChannel.id;\n    const key = [cKey, mockGuild.id];\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData(key, mockChannelList)}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentChannel(channelId, mockGuild.id), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current?.id).toEqual(channelId);\n      expect(result.current).toEqual(mockChannel);\n    } finally {\n      unmount();\n    }\n  });\n\n  it('returns undefined if it cannot find a channel with the given id', async () => {\n    const channelId = mockChannel.id;\n    const key = [cKey, mockGuild.id];\n    const channel: Channel = {\n      id: '12345676890345345',\n      name: 'Guild Name',\n      hasNotification: false,\n      isPublic: true,\n    };\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData(key, [channel])}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentChannel(channelId, mockGuild.id), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current).toBeUndefined();\n    } finally {\n      unmount();\n    }\n  });\n\n  it('returns undefined if there is not initial data', async () => {\n    const channelId = mockChannel.id;\n    const key = [cKey, mockGuild.id];\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData(key, [])}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentChannel(channelId, mockGuild.id), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current).toBeUndefined();\n    } finally {\n      unmount();\n    }\n  });\n});\n"
  },
  {
    "path": "web/src/tests/queries/dm.test.tsx",
    "content": "import { renderHook } from '@testing-library/react-hooks';\nimport { QueryClientProvider, useQuery } from '@tanstack/react-query';\nimport { rest } from 'msw';\nimport * as React from 'react';\nimport { dmKey } from '../../lib/utils/querykeys';\nimport { createQueryClientWrapper, createTestQueryClientWithData, IQueryWrapperProps } from '../testUtils';\nimport { server } from '../../setupTests';\nimport { getUserDMs } from '../../lib/api/handler/dm';\nimport { mockDMChannel, mockDMChannelList } from '../fixture/dmFixtures';\nimport { useGetCurrentDM } from '../../lib/utils/hooks/useGetCurrentDM';\nimport { DMChannel } from '../../lib/models/dm';\n\ndescribe('useQuery - getUserDMs', () => {\n  it(\"successfully fetches the user's dm list\", async () => {\n    const { result, waitForNextUpdate } = renderHook(\n      () =>\n        useQuery([dmKey], async () => {\n          const { data } = await getUserDMs();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    expect(result.current.isFetching).toBe(true);\n    await waitForNextUpdate();\n\n    expect(result.current.isFetching).toBe(false);\n    expect(result.current.isSuccess).toBe(true);\n\n    expect(result.current.data).not.toBeNull();\n    expect(result.current.data).not.toBeUndefined();\n    expect(result.current.data?.length).toEqual(1);\n\n    const dm = result.current.data?.[0];\n    expect(dm).toEqual(mockDMChannel);\n    expect(dm?.id).toBeDefined();\n    expect(dm?.user).toBeDefined();\n\n    const user = dm?.user;\n    expect(user?.id).toBeDefined();\n    expect(user?.image).toBeDefined();\n    expect(user?.username).toBeDefined();\n    expect(user?.isOnline).toBeDefined();\n  });\n\n  it('returns an error when the server returns status 500', async () => {\n    server.use(rest.get('*', (req, res, ctx) => res(ctx.status(500))));\n\n    const { result, waitFor } = renderHook(\n      () =>\n        useQuery([dmKey], async () => {\n          const { data } = await getUserDMs();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    await waitFor(() => result.current.isError);\n\n    expect(result.current.error).toBeDefined();\n    expect(result.current.data).toBeUndefined();\n  });\n});\n\ndescribe('useGetCurrentDM', () => {\n  it('successfully fetches the dm for the given ID', async () => {\n    const channelId = mockDMChannel.id;\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([dmKey], mockDMChannelList)}>\n        {children}\n      </QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentDM(channelId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current?.id).toEqual(channelId);\n      expect(result.current).toEqual(mockDMChannel);\n    } finally {\n      unmount();\n    }\n  });\n\n  it('returns undefined if it cannot find a dm with the given id', async () => {\n    const channelId = mockDMChannel.id;\n    const dmChannel: DMChannel = {\n      id: '12345676890345345',\n      user: {\n        image: '',\n        isOnline: true,\n        isFriend: false,\n        username: 'Test User',\n        id: '123941059157915',\n      },\n    };\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([dmKey], [dmChannel])}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentDM(channelId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current).toBeUndefined();\n    } finally {\n      unmount();\n    }\n  });\n\n  it('returns undefined if there is not initial data', async () => {\n    const channelId = mockDMChannel.id;\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([dmKey], [])}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentDM(channelId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current).toBeUndefined();\n    } finally {\n      unmount();\n    }\n  });\n});\n"
  },
  {
    "path": "web/src/tests/queries/friend.test.tsx",
    "content": "import { renderHook } from '@testing-library/react-hooks';\nimport { QueryClientProvider, useQuery } from '@tanstack/react-query';\nimport { rest } from 'msw';\nimport * as React from 'react';\nimport { createQueryClientWrapper, createTestQueryClientWithData, IQueryWrapperProps } from '../testUtils';\nimport { fKey } from '../../lib/utils/querykeys';\nimport { server } from '../../setupTests';\nimport { getFriends } from '../../lib/api/handler/account';\nimport { mockFriend, mockFriendList } from '../fixture/friendFixture';\nimport { useGetFriend } from '../../lib/utils/hooks/useGetFriend';\n\ndescribe('useQuery - getFriends', () => {\n  it(\"successfully fetches the user's friend list\", async () => {\n    const { result, waitForNextUpdate } = renderHook(\n      () =>\n        useQuery([fKey], async () => {\n          const { data } = await getFriends();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    expect(result.current.isFetching).toBe(true);\n    await waitForNextUpdate();\n\n    expect(result.current.isFetching).toBe(false);\n    expect(result.current.isSuccess).toBe(true);\n\n    expect(result.current.data).not.toBeNull();\n    expect(result.current.data).not.toBeUndefined();\n    expect(result.current.data?.length).toEqual(1);\n\n    const friend = result.current.data?.[0];\n    expect(friend).toEqual(mockFriend);\n    expect(friend?.id).toBeDefined();\n    expect(friend?.username).toBeDefined();\n    expect(friend?.image).toBeDefined();\n    expect(friend?.isOnline).toBeDefined();\n  });\n\n  it('returns an error when the server returns status 500', async () => {\n    server.use(rest.get('*', (req, res, ctx) => res(ctx.status(500))));\n\n    const { result, waitFor } = renderHook(\n      () =>\n        useQuery([fKey], async () => {\n          const { data } = await getFriends();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    await waitFor(() => result.current.isError);\n\n    expect(result.current.error).toBeDefined();\n    expect(result.current.data).toBeUndefined();\n  });\n});\n\ndescribe('useGetFriend', () => {\n  it('successfully fetches the friend for the given ID', async () => {\n    const friendId = mockFriend.id;\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([fKey], mockFriendList)}>\n        {children}\n      </QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetFriend(friendId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current?.id).toEqual(friendId);\n      expect(result.current).toEqual(mockFriend);\n    } finally {\n      unmount();\n    }\n  });\n\n  it('returns undefined if it cannot find a friend with the given id', async () => {\n    const friendId = mockFriend.id;\n    const friend = {\n      id: '12345676890345345',\n      username: 'Test User',\n      image: 'https://gravatar.com/avatar/c160f8cc69a4f0bf2b0362752353d060?d=identicon',\n      isOnline: false,\n    };\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([fKey], [friend])}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetFriend(friendId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current).toBeUndefined();\n    } finally {\n      unmount();\n    }\n  });\n\n  it('returns undefined if there is not initial data', async () => {\n    const friendId = mockFriend.id;\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([fKey], [])}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetFriend(friendId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current).toBeUndefined();\n    } finally {\n      unmount();\n    }\n  });\n});\n"
  },
  {
    "path": "web/src/tests/queries/guild.test.tsx",
    "content": "import { renderHook } from '@testing-library/react-hooks';\nimport { QueryClientProvider, useQuery } from '@tanstack/react-query';\nimport { rest } from 'msw';\nimport * as React from 'react';\nimport { createQueryClientWrapper, createTestQueryClientWithData, IQueryWrapperProps } from '../testUtils';\nimport { gKey } from '../../lib/utils/querykeys';\nimport { getUserGuilds } from '../../lib/api/handler/guilds';\nimport { server } from '../../setupTests';\nimport { useGetCurrentGuild } from '../../lib/utils/hooks/useGetCurrentGuild';\nimport { mockGuild, mockGuildList } from '../fixture/guildFixtures';\nimport { Guild } from '../../lib/models/guild';\n\ndescribe('useQuery - getUserGuilds', () => {\n  it(\"successfully fetches the user's guild list\", async () => {\n    const { result, waitForNextUpdate } = renderHook(\n      () =>\n        useQuery([gKey], async () => {\n          const { data } = await getUserGuilds();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    expect(result.current.isFetching).toBe(true);\n    await waitForNextUpdate();\n\n    expect(result.current.isFetching).toBe(false);\n    expect(result.current.isSuccess).toBe(true);\n\n    expect(result.current.data).not.toBeNull();\n    expect(result.current.data).not.toBeUndefined();\n    expect(result.current.data?.length).toEqual(1);\n\n    const guild = result.current.data?.[0];\n    expect(guild).toEqual(mockGuild);\n    expect(guild?.id).toBeDefined();\n    expect(guild?.name).toBeDefined();\n    expect(guild?.ownerId).toBeDefined();\n    expect(guild?.hasNotification).toBeFalsy();\n    expect(guild?.default_channel_id).toBeDefined();\n  });\n\n  it('returns an error when the server returns status 500', async () => {\n    server.use(rest.get('*', (req, res, ctx) => res(ctx.status(500))));\n\n    const { result, waitFor } = renderHook(\n      () =>\n        useQuery([gKey], async () => {\n          const { data } = await getUserGuilds();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    await waitFor(() => result.current.isError);\n\n    expect(result.current.error).toBeDefined();\n    expect(result.current.data).toBeUndefined();\n  });\n});\n\ndescribe('useGetCurrentGuild', () => {\n  it('successfully fetches the guild for the given ID', async () => {\n    const guildId = mockGuild.id;\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([gKey], mockGuildList)}>\n        {children}\n      </QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentGuild(guildId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current?.id).toEqual(guildId);\n    } finally {\n      unmount();\n    }\n  });\n\n  it('returns undefined if it cannot find a guild with the given id', async () => {\n    const guildId = mockGuild.id;\n    const guild: Guild = {\n      id: '12345676890345345',\n      name: 'Guild Name',\n      default_channel_id: '149587609049385',\n      ownerId: '123941059157915',\n    };\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([gKey], [guild])}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentGuild(guildId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current).toBeUndefined();\n    } finally {\n      unmount();\n    }\n  });\n\n  it('returns undefined if there is not initial data', async () => {\n    const guildId = mockGuild.id;\n\n    const wrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n      <QueryClientProvider client={createTestQueryClientWithData([gKey], [])}>{children}</QueryClientProvider>\n    );\n\n    const { result, unmount } = renderHook(() => useGetCurrentGuild(guildId), {\n      wrapper,\n    });\n\n    try {\n      expect(result.current).toBeUndefined();\n    } finally {\n      unmount();\n    }\n  });\n});\n"
  },
  {
    "path": "web/src/tests/queries/member.test.tsx",
    "content": "import { renderHook } from '@testing-library/react-hooks';\nimport { useQuery } from '@tanstack/react-query';\nimport { rest } from 'msw';\nimport { mKey } from '../../lib/utils/querykeys';\nimport { createQueryClientWrapper } from '../testUtils';\nimport { server } from '../../setupTests';\nimport { getGuildMembers } from '../../lib/api/handler/guilds';\nimport { mockGuild } from '../fixture/guildFixtures';\nimport { mockMember } from '../fixture/memberFixtures';\n\ndescribe('useQuery - getGuildMembers', () => {\n  it(\"successfully fetches the guild's member list\", async () => {\n    const guildId = mockGuild.id;\n\n    const { result, waitForNextUpdate } = renderHook(\n      () =>\n        useQuery([mKey, guildId], async () => {\n          const { data } = await getGuildMembers(guildId);\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    expect(result.current.isFetching).toBe(true);\n    await waitForNextUpdate();\n\n    expect(result.current.isFetching).toBe(false);\n    expect(result.current.isSuccess).toBe(true);\n\n    expect(result.current.data).not.toBeNull();\n    expect(result.current.data).not.toBeUndefined();\n\n    const member = result.current.data?.[0];\n    expect(member).toEqual(mockMember);\n    expect(member?.id).toBeDefined();\n    expect(member?.username).toBeDefined();\n    expect(member?.image).toBeDefined();\n    expect(member?.isOnline).toBeDefined();\n    expect(member?.isFriend).toBeDefined();\n  });\n\n  it('returns an error when the server returns status 500', async () => {\n    const guildId = mockGuild.id;\n    server.use(rest.get('*', (req, res, ctx) => res(ctx.status(500))));\n\n    const { result, waitFor } = renderHook(\n      () =>\n        useQuery([mKey, guildId], async () => {\n          const { data } = await getGuildMembers(guildId);\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    await waitFor(() => result.current.isError);\n\n    expect(result.current.error).toBeDefined();\n    expect(result.current.data).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "web/src/tests/queries/message.test.tsx",
    "content": "import { renderHook } from '@testing-library/react-hooks';\nimport { useInfiniteQuery } from '@tanstack/react-query';\nimport { rest } from 'msw';\nimport { createQueryClientWrapper } from '../testUtils';\nimport { server } from '../../setupTests';\nimport { getMessages } from '../../lib/api/handler/messages';\nimport { mockChannel } from '../fixture/channelFixtures';\nimport { mockMessageList } from '../fixture/messageFixtures';\nimport { Message } from '../../lib/models/message';\n\ndescribe('useQuery - getMessages', () => {\n  it(\"successfully fetches the channel's message list\", async () => {\n    const channelId = mockChannel.id;\n    const qKey = 'messages';\n\n    const { result, waitForNextUpdate } = renderHook(\n      () =>\n        useInfiniteQuery<Message[]>(\n          [qKey, channelId],\n          async ({ pageParam = null }) => {\n            const { data: messageData } = await getMessages(channelId, pageParam);\n            return messageData;\n          },\n          {\n            staleTime: 0,\n            cacheTime: 0,\n            getNextPageParam: (lastPage) => (lastPage.length ? lastPage[lastPage.length - 1].createdAt : ''),\n          }\n        ),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    expect(result.current.isFetching).toBe(true);\n    await waitForNextUpdate();\n\n    expect(result.current.isFetching).toBe(false);\n    expect(result.current.isSuccess).toBe(true);\n\n    expect(result.current.data).not.toBeNull();\n    expect(result.current.data).not.toBeUndefined();\n\n    expect(result.current.data).toEqual({ pageParams: [undefined], pages: [mockMessageList] });\n  });\n\n  it('returns an error when the server returns status 500', async () => {\n    const channelId = mockChannel.id;\n    const qKey = 'messages';\n    server.use(rest.get('*', (req, res, ctx) => res(ctx.status(500))));\n\n    const { result, waitFor } = renderHook(\n      () =>\n        useInfiniteQuery<Message[]>(\n          [qKey, channelId],\n          async ({ pageParam = null }) => {\n            const { data: messageData } = await getMessages(channelId, pageParam);\n            return messageData;\n          },\n          {\n            staleTime: 0,\n            cacheTime: 0,\n            getNextPageParam: (lastPage) => (lastPage.length ? lastPage[lastPage.length - 1].createdAt : ''),\n          }\n        ),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    await waitFor(() => result.current.isError);\n\n    expect(result.current.error).toBeDefined();\n    expect(result.current.data).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "web/src/tests/queries/request.test.tsx",
    "content": "import { renderHook } from '@testing-library/react-hooks';\nimport { useQuery } from '@tanstack/react-query';\nimport { rest } from 'msw';\nimport { rKey } from '../../lib/utils/querykeys';\nimport { createQueryClientWrapper } from '../testUtils';\nimport { server } from '../../setupTests';\nimport { getPendingRequests } from '../../lib/api/handler/account';\nimport { mockRequest } from '../fixture/requestFixtures';\n\ndescribe('useQuery - getPendingRequests', () => {\n  it(\"successfully fetches the user's pending requests\", async () => {\n    const { result, waitForNextUpdate } = renderHook(\n      () =>\n        useQuery([rKey], async () => {\n          const { data } = await getPendingRequests();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    expect(result.current.isFetching).toBe(true);\n    await waitForNextUpdate();\n\n    expect(result.current.isFetching).toBe(false);\n    expect(result.current.isSuccess).toBe(true);\n\n    expect(result.current.data).not.toBeNull();\n    expect(result.current.data).not.toBeUndefined();\n\n    const request = result.current.data?.[0];\n    expect(request).toEqual(mockRequest);\n    expect(request?.id).toBeDefined();\n    expect(request?.username).toBeDefined();\n    expect(request?.image).toBeDefined();\n    expect(request?.type).toBeDefined();\n  });\n\n  it('returns an error when the server returns status 500', async () => {\n    server.use(rest.get('*', (req, res, ctx) => res(ctx.status(500))));\n\n    const { result, waitFor } = renderHook(\n      () =>\n        useQuery([rKey], async () => {\n          const { data } = await getPendingRequests();\n          return data;\n        }),\n      {\n        wrapper: createQueryClientWrapper,\n      }\n    );\n\n    await waitFor(() => result.current.isError);\n\n    expect(result.current.error).toBeDefined();\n    expect(result.current.data).toBeUndefined();\n  });\n});\n"
  },
  {
    "path": "web/src/tests/testUtils.tsx",
    "content": "import { rest } from 'msw';\nimport * as React from 'react';\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query';\nimport { mockGuildList } from './fixture/guildFixtures';\nimport { mockDMChannelList } from './fixture/dmFixtures';\nimport { mockAccount } from './fixture/accountFixture';\nimport { mockFriendList } from './fixture/friendFixture';\nimport { mockRequestList } from './fixture/requestFixtures';\nimport { mockChannelList } from './fixture/channelFixtures';\nimport { mockMemberList } from './fixture/memberFixtures';\nimport { mockMessageList } from './fixture/messageFixtures';\n\nconst createTestQueryClient = (): QueryClient =>\n  new QueryClient({\n    defaultOptions: {\n      queries: {\n        retry: false,\n      },\n    },\n    logger: {\n      // eslint-disable-next-line no-console\n      log: console.log,\n      // eslint-disable-next-line no-console\n      warn: console.warn,\n      error: () => {},\n    },\n  });\n\nexport const createTestQueryClientWithData = (key: string[], data: any[]): QueryClient => {\n  const client = new QueryClient({\n    defaultOptions: {\n      queries: {\n        retry: false,\n      },\n    },\n    logger: {\n      // eslint-disable-next-line no-console\n      log: console.log,\n      // eslint-disable-next-line no-console\n      warn: console.warn,\n      error: () => {},\n    },\n  });\n  client.setQueryData(key, () => [...data]);\n\n  return client;\n};\n\nexport interface IQueryWrapperProps {\n  children: React.ReactNode;\n}\n\nexport const createQueryClientWrapper: React.FC<IQueryWrapperProps> = ({ children }) => (\n  <QueryClientProvider client={createTestQueryClient()}>{children}</QueryClientProvider>\n);\n\nexport const handlers = [\n  rest.get('*/guilds', (req, res, ctx) => res(ctx.status(200), ctx.json(mockGuildList))),\n  rest.get('*/channels/me/dm', (req, res, ctx) => res(ctx.status(200), ctx.json(mockDMChannelList))),\n  rest.get('*/account', (req, res, ctx) => res(ctx.status(200), ctx.json(mockAccount))),\n  rest.get('*/account/me/friends', (req, res, ctx) => res(ctx.status(200), ctx.json(mockFriendList))),\n  rest.get('*/account/me/pending', (req, res, ctx) => res(ctx.status(200), ctx.json(mockRequestList))),\n  rest.get('*/channels/*', (req, res, ctx) => res(ctx.status(200), ctx.json(mockChannelList))),\n  rest.get('*/guilds/*/members', (req, res, ctx) => res(ctx.status(200), ctx.json(mockMemberList))),\n  rest.get('*/messages/*', (req, res, ctx) => res(ctx.status(200), ctx.json(mockMessageList))),\n];\n"
  },
  {
    "path": "web/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"esModuleInterop\": true,\n    \"allowSyntheticDefaultImports\": true,\n    \"strict\": true,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\"\n  },\n  \"include\": [\n    \"src\"\n  ]\n}\n"
  }
]