Skip to content
This repository was archived by the owner on Sep 14, 2021. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# DATABASE_URL=file:./dev.db
# TEST_DATABASE_URL=file:./.redwood/test.db
# PRISMA_HIDE_UPDATE_MESSAGE=true
COLLAB_LAND_APP_ID=
COLLAB_LAND_APP_SECRET=
ETHEREUM_JWT_SECRET=
XDAI_RPC_URL=
INFURA_ENDPOINT_KEY=

LOGIN_URL=http://0.0.0.0:8910/login

#DATABASE_URL=file:./dev.db
7 changes: 0 additions & 7 deletions .env.template

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ dist-babel
node_modules
yarn-error.log
web/public/mockServiceWorker.js
notes.md
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 Redwood
Copyright (c) 2020 Patrick Gallagher

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
77 changes: 55 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,61 +1,83 @@
<h1 align="center">Welcome to unlock-protocol-discord-bot 👋</h1>
<h1 align="center">Welcome swordy-bot 👋</h1>
<p>
<a href="#" target="_blank">
<img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-yellow.svg" />
</a>
</p>

> A Discord bot which prompts the user to enter their DID, and creates a new challenge for them.
> An Ethereum wallet verification service for role-gated Discord servers.

## Whats included

- **Discord bot** w/ docker container in `/bot`
- **API server** RedwoodJS API in `/api`
- **(incomplete) Frontend** RedwoodJS web app in `/web`
- **🤖 Discord bot `/bot`**
- **🛰️ Redwood api server `/api`**
- **🖥️ Redwood web app `/web`**

## Local Development
## Development

Setup the backend + frontend
Install both the app and bot

```bash
# Redwood
yarn

# Initialize SQL-lite and perform initial migrations
yarn rw db save
yarn rw db up

# Start the API service and Frontend
yarn rw dev
# Discord Bot
cd bot && yarn
```

Start the bot

```bash
cd bot && yarn
# In /bot, update the variables here
cp .env.template .env

yarn start
```

Set up the Redwood App

```bash
# In root of the repo, update the variables here
cp .env.template .env


# Create a new database and perform migrations
yarn rw prisma migrate dev
```

Start Prisma Studio (database admin tool)

```bash
yarn rw prisma studio
```

Start both the Redwood frontend and api

```bash
yarn rw dev
```

## Going to Production

The easiest way to deploy is using Vercel for the frontend and backend, and Heroku for the bot and postgres database. Hosting this way is free, and can be setup in about 10 minutes.

In order to collect all the required environment variables, you'll need to deploy all three parts (bot, FE+BE, discord app), and go back and update as necessary.
In order to collect all the required environment variables, you'll need to deploy all three parts (bot, FE+BE, discord app), then go back and update the env variables as necessary.

### Vercel - Backend + Frontend

Point vercel to your repo, and deploy. Redwood build commands should be auto-configured.
Create a new Vercel app using your forked repo. The build commands are automatically detected for RedwoodJS projects.

Update the environment variables for what you see in the root `.env` file. The only new one is `DATABASE_URL` which you need from your Heroku database in the next step.
Update the environment variables for what you see in the root `.env` file. You will get `DATABASE_URL` from your Heroku database in the next step.

### Heroku - Bot

Heroku doesn't like apps that aren't in the root folder. To get around this, I added a `heroku-prebuild` script which installs the bot dependencies. The Procfile runs the `bot.js`

Once deployed, head to the "Resources" tab, turn off the `web` Dyno, and turn on the `worker` Dyno. This Dyno is defined in the repo root `Procfile`.
Once deployed, head to the "Resources" tab for this Heroku app, turn off the `web` Dyno, and turn on the `worker` Dyno. This Dyno is defined in the repo root `Procfile`.

Update the environment variables in "Settings" tab to reflect what you see in the `.env` here.

Add a new postgres add-on and add the url to the Vercel environment.
Create a new **postgres add-on** in this Heroku app, and add the url to the Vercel environment as `DATABASE_URL`.

### Discord

Expand All @@ -70,13 +92,20 @@ Copy the `TOKEN` and add it to your bot app.
Now add the bot to your server. In the Discord Application, in "General Information", copy the `CLIENT ID`. Insert it in this URL, and have the server administrator open it.

```

# Add the bot with role management permissions

https://discord.com/oauth2/authorize?client_id=<clientID>&scope=bot&permissions=268435456

```

## Docker option

If you want to avoid Heroku for the bot, you can use docker. If you've made changes to the bot, you'll need to generate a new Docker image.
If you want to avoid Heroku for the bot, you can use docker.

> Note: avoiding Heroku means you'll also need to bring your own postgres database. See the docs for [self-hosting redwood](https://redwoodjs.com/cookbook/self-hosting-redwood.html#self-hosting-redwood).

If you've made changes to the bot, generate your own Docker image.

```bash
# Build
Expand All @@ -98,10 +127,10 @@ source .env
docker-compose up -d
```

## Notes
## Troubleshooting + Tips

- If you have permissions errors, try giving the bot a higher role. Bots can only give roles to members in a _lower position_ than their own highest role. See https://discord.com/developers/docs/topics/permissions#permission-hierarchy
- Helpful discord docs for making a "emoji-reaction" menu https://discordjs.guide/popular-topics/reactions.html#awaiting-reactions
- Make an "emoji-reaction" menu https://discordjs.guide/popular-topics/reactions.html#awaiting-reactions

## Author

Expand All @@ -115,6 +144,10 @@ docker-compose up -d

Give a ⭐️ if this project helped you!

## Sponsors

Big thanks to the [Unlock Protocol](unlock-protocol.com) developer grants program for providing support for this project!

---

_This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_
1 change: 1 addition & 0 deletions api/db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ model User {
guilds Guild[] @relation(name: "userGuilds")
roles Role[] @relation(name: "userRoles")
currentSessionGuild Guild?
ephemeralId String?
}

model Guild {
Expand Down
15 changes: 15 additions & 0 deletions api/src/graphql/bot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const schema = gql`
type Response {
type: String!
text: String
url: String
}
type Query {
postMessage(
content: String!
platformUserId: String!
platform: String!
guildId: String!
): Response!
}
`
6 changes: 1 addition & 5 deletions api/src/graphql/users.sdl.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,8 @@ export const schema = gql`
type Query {
users: [User!]!
user(id: String!): User
loginSuccess(address: String!): User
haveUserAddress(platformId: String!): HaveUserAddress
userByPlatformId(
platformId: String!
platform: String!
guildId: String
): LimitedScopeUser
userByDiscordId(discordId: String!): User
}

Expand Down
37 changes: 37 additions & 0 deletions api/src/lib/bot/bot.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { db } from 'src/lib/db'
import { v4 as uuidv4 } from 'uuid'
import { LOGIN_URL, DISCORD_INITIAL_AUTH } from 'src/lib/bot/constants'

export const handleMessage = async ({
content,
platformUserId,
platform,
guildId,
}) => {
// TODO: is this always an invocation?
// Create the user in the database
const ephemeralId = uuidv4()
const user = await db.user.upsert({
where: { platformId: platformUserId },
create: {
platformId: platformUserId,
platform,
currentSessionGuild: {
connect: { platformId: guildId },
},
ephemeralId,
},
update: {
currentSessionGuild: {
connect: { platformId: guildId },
},
ephemeralId,
},
})
// Return the unique URL for the response
return {
text: DISCORD_INITIAL_AUTH + LOGIN_URL + ephemeralId,
type: 'reply',
url: null,
}
}
7 changes: 7 additions & 0 deletions api/src/lib/bot/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const UNLOCKED_ROLE_BASE = 'Unlocked-Holder'
export const CHIEV_ROLE_BASE = 'one-snoo-club'
export const LOGIN_URL = `${process.env.LOGIN_URL}?ephemeralId=`

export const DISCORD_INITIAL_AUTH = `⚔️ Ready to be knighted? \n\nJust one more thing I need from you:
\n-\`Ethereum wallet address\`
\n Click the link to login using your wallet:\n`
109 changes: 109 additions & 0 deletions api/src/lib/bot/oldFiles/apiMgr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
const { ApolloClient } = require('apollo-client')
const { InMemoryCache } = require('apollo-cache-inmemory')
const { HttpLink } = require('apollo-link-http')
const fetch = require('cross-fetch')
const {
rolesByUserAndGuild,
haveUserAddress,
GET_OR_CREATE_USER_MUTATION,
POST_MESSAGE_QUERY,
} = require('./graphql-operations/queries')
const { updateRole } = require('./graphql-operations/mutations')

class ApiMgr {
constructor() {
const debug = true
const cache = new InMemoryCache()
const link = new HttpLink({
uri: process.env.API_URL,
fetch,
})
this.client = new ApolloClient({
link,
cache,
onError: (e) => {
debug && console.log(e)
},
defaultOptions: {
query: {
fetchPolicy: 'network-only',
},
},
})
}

async postMessage({ message }) {
try {
const res = await this.client.query({
query: POST_MESSAGE_QUERY,
variables: {
content: message.content,
platformUserId: message.member.id,
platform: 'discord',
guildId: message.guild.id,
},
})
return res.data.response
} catch (e) {
console.log(e)
throw new Error(e)
}
}
////////////////////////////////////////////
// TODO: Move to API
async userByPlatformId({ platformId, platform, guildId }) {
try {
const res = await this.client.mutate({
query: GET_OR_CREATE_USER_MUTATION,
variables: { platformId, platform, guildId },
})
return res.data.userByPlatformId
} catch (e) {
console.log(e)
throw new Error(e)
}
}

async getRolesByUserAndGuild({ platformId, guildId }) {
if (!guildId || !platformId) throw new Error('no platformId or guildId')
try {
// TODO: send all guild roles, so we can remove them from db if they were deleted
const res = await this.client.query({
query: rolesByUserAndGuild,
variables: { platformId, guildId },
})
return { roles: res.data.rolesByUserAndGuild }
} catch (e) {
console.log(e)
throw new Error(e)
}
}

async getNfts(discordId) {
if (!discordId) throw new Error('no discordId')
try {
const user = await this.client.query({
query: userByDiscordId,
variables: { discordId },
})
return { nfts: user.data.userByDiscordId.nfts }
} catch (e) {
console.log(e)
throw new Error(e)
}
}

async updateRole(input) {
try {
await this.client.mutate({
mutation: updateRole,
variables: { ...input },
})
} catch (e) {
console.log(e)
throw new Error(e)
}
}
}

module.exports = ApiMgr
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@ const {
DISCORD_GUILD_DOESNT_HAVE_ROLE,
} = require('../textContent')

const verifyInstall = (guild) => {
if (!guild.me.hasPermission(['MANAGE_ROLES']))
throw Error('#' + DISCORD_INVALID_PERMISSIONS)
}

const getRoleFromName = ({ name, guild }) => {
const role = guild.roles.cache.find((role) => role.name === name)
if (!role)
Expand Down
Loading