Browse Source

Merge branch 'character_apps' of PMD/rotom_bot into master

kjswis 6 years ago
parent
commit
ee0235d2a1

+ 9 - 1
README.md

@@ -13,12 +13,20 @@ you may create a pull request at [ajsw.is](https://code.ajsw.is/PMD/rotom_bot)
 
 ```bash
 $ git checkout -b [branch_name]       #to create a branch
-$ git push origin  branch_name]       #to push branch
+$ git push origin [branch_name]       #to push branch
+```
+
+In order to import a schema, use the following command. To learn more about
+Postgres, visit the wiki tab or [postgresql.org](https://www.postgresql.org/docs/)
+```bash
+$ psql [db_name] < db/schema.sql
 ```
 
 ### Features
   * Says Hello
   * Displays Type Matchups
+  * Communicates with Users to Create and Edit Character Applications
+  * Uses Reactions to Approve or Deny Character Applications
 
 ## Setup
 This application runs using Ruby and Postgres. In order to run the bot locally

+ 14 - 0
app/controllers/character_controller.rb

@@ -0,0 +1,14 @@
+class CharacterController
+  def self.edit_character(params)
+    char_hash = Character.from_form(params)
+
+    if char = Character.find_by(edit_url: char_hash["edit_url"])
+      char.update!(char_hash)
+      character = Character.find_by(edit_url: char_hash["edit_url"])
+    else
+      character = Character.create(char_hash)
+    end
+
+    character
+  end
+end

+ 15 - 0
app/controllers/image_controller.rb

@@ -0,0 +1,15 @@
+class ImageController
+  def self.edit_images(content, char_id)
+    if image_url = /\*\*URL to the Character\'s Appearance\*\*\:\s(.*)/.match(content)
+      unless CharImages.where(char_id: char_id).find_by(url: image_url[1])
+        image = CharImages.create(char_id: char_id, url: image_url[1], category: 'SFW', keyword: 'Primary')
+      end
+    end
+
+    if image
+      image.url
+    else
+      image_url[1]
+    end
+  end
+end

+ 24 - 0
app/models/char_app_responses.rb

@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module CharAppResponses
+  GRAMMAR = "Please check your grammar and\ncapitalization"
+  UNITS = "Please specify your units in\nImperial or Metric"
+  IMAGE = "Your image is inappropriate for\ndefault use"
+  LORE = "One or more responses are\nconflicting with server lore"
+  UNDER_AGE = "Your age conflicts with the\nspecified rating"
+  INVALID = "One or more responses are\ninvalid"
+  VULGAR = "Your application is too vulgar,\nor conflicts with server rules"
+  DM_NOTES = "Please elaborate on your\nDM Notes"
+  INLINE_SPACE = "------------------------------------"
+
+  REJECT_MESSAGES = {
+    Emoji::SPEECH_BUBBLE => GRAMMAR,
+    Emoji::SCALE => UNITS,
+    Emoji::PICTURE => IMAGE,
+    Emoji::BOOKS => LORE,
+    Emoji::BABY => UNDER_AGE,
+    Emoji::SKULL => INVALID,
+    Emoji::VULGAR => VULGAR,
+    Emoji::NOTE => DM_NOTES
+  }
+end

+ 4 - 0
app/models/char_images.rb

@@ -0,0 +1,4 @@
+class CharImages < ActiveRecord::Base
+  validates :char_id, presence: true
+  validates :url, presence: true
+end

+ 107 - 0
app/models/characters.rb

@@ -0,0 +1,107 @@
+class Character < ActiveRecord::Base
+  validates :user_id, presence: true
+  validates :name, presence: true
+  validates :species, presence: true
+  validates :types, presence: true
+
+  def self.from_form(params)
+    key_mapping = {
+      "_New Character Application_" => "active",
+      "Submitted by" => "user_id",
+      " >>> **Characters Name**" => "name",
+      "**Species**" => "species",
+      "**Type**" => "types",
+      "**Age**" => "age",
+      "**Weight**" => "weight",
+      "**Height**" => "height",
+      "**Gender**" => "gender",
+      "**Sexual Orientation**" => "orientation",
+      "**Relationship Status**" => "relationship",
+      "**Attacks**" => "attacks",
+      "**Likes**" => "likes",
+      "**Dislikes**" => "dislikes",
+      "**Personality**" => "personality",
+      "**Hometown**" => "hometown",
+      "**Warnings**" => "warnings",
+      "**Rumors**" => "rumors",
+      "**Backstory**" => "backstory",
+      "**Other**" => "other",
+      "**Rating**" => "rating",
+      "**Current Location**" => "location",
+      "**DM Notes**" => "dm_notes",
+      "Edit Key (ignore)" => "edit_url",
+    }
+
+    hash = {
+      "user_id" => nil,
+      "name" => nil,
+      "species" => nil,
+      "types" => nil,
+      "age" => nil,
+      "weight" => nil,
+      "height" => nil,
+      "gender" => nil,
+      "orientation" => nil,
+      "relationship" => nil,
+      "attacks" => nil,
+      "likes" => nil,
+      "dislikes" => nil,
+      "personality" => nil,
+      "backstory" => nil,
+      "other" => nil,
+      "edit_url" => nil,
+      "active" => nil,
+      "dm_notes" => nil,
+      "location" => nil,
+      "rumors" => nil,
+      "hometown" => nil,
+      "warnings" => nil,
+      "rating" => nil
+    }
+
+    params.map do |item|
+      next if item.empty?
+
+      key,value = item.split(": ")
+      db_column = key_mapping[key]
+
+      if db_column == "active" && value == "Personal Character"
+        hash[db_column] = "Active"
+      elsif v = value.match(/<@([0-9]+)>/)
+        hash[db_column] = v[1]
+      else
+        hash[db_column] = value
+      end
+    end
+
+    hash = hash.reject { |k,v| k == nil }
+    hash
+  end
+
+  def self.check_user(event)
+    content = event.message.content
+    edit_url = /Edit\sKey\s\(ignore\):\s([\s\S]*)/.match(content)
+    active = /\_New\sCharacter\sApplication\_:\s(.*)/.match(content)
+    user_id = /<@([0-9]+)>/.match(content)
+
+    user = User.find_by(id: user_id[1])
+    member = event.server.member(user_id[1])
+    if user && member
+
+      if active[1] == "Personal Character"
+        allowed_characters = (user.level / 10 + 1)
+        characters = Character.where(user_id: user_id[1]).where(active: "Active").count
+
+        if characters < allowed_characters && characters < 6
+          approval_react(event)
+        else
+          too_many(event, member, edit_url, 'characters')
+        end
+      else
+        approval_react(event)
+      end
+    else
+      unknown_member(event)
+    end
+  end
+end

+ 3 - 0
app/models/users.rb

@@ -0,0 +1,3 @@
+class User < ActiveRecord::Base
+  validates :id, presence: true
+end

+ 56 - 0
app/responses/app.rb

@@ -0,0 +1,56 @@
+def new_app_embed(event)
+  name = event.author.nickname || event.author.name
+
+  chat_embed = Embed.new(
+    title: "New Application!",
+    description: "Hi, #{name},\nI see you'd like to start a new character application!\nI'll send you instructions in a dm!",
+    color: event.author.color.combined
+  )
+
+  embed = Embed.new(
+    title: "Hi, #{name}",
+    description: "If you have any questions, please feel free to ask a Guildmaster!",
+    color: event.author.color.combined,
+    fields: [
+      { name: "Please start your application here:", value: APP_FORM },
+      { name: "Your key is:", value: event.author.id }
+    ]
+  )
+
+  event.send_embed("", chat_embed)
+  embed
+end
+
+def edit_app_embed(event, edit_url, char_name)
+  name = event.author.nickname || event.author.name
+
+  chat_embed = Embed.new(
+    title: "You want to edit #{char_name}?",
+    description: "Good news, #{name}! I'll dm you a link",
+    color: event.author.color.combined
+  )
+
+  embed = Embed.new(
+    title: "You may edit #{char_name} here:",
+    description: edit_url,
+    color: event.author.color.combined
+  )
+
+  event.send_embed("", chat_embed)
+  embed
+end
+
+def app_not_found_embed(event, char_name)
+  name = event.author.nickname || event.author.name
+
+  embed = Embed.new(
+    title: "I'm sorry, #{name}",
+    description: "I can't seem to find your character named, #{char_name}",
+    color: "#a41e1f",
+    fields: [
+      { name: "Want to start a new application?", value: "You can start one with this command:\n```pkmn-app```"}
+    ]
+  )
+
+  event.send_embed("", embed)
+end

+ 33 - 0
app/responses/application_responses.rb

@@ -0,0 +1,33 @@
+require_relative '../../lib/emoji.rb'
+
+def approval_react(event)
+  event.message.react(Emoji::Y)
+  event.message.react(Emoji::N)
+end
+
+def too_many(event, user, edit_url, model)
+  event.server.member(user).dm("You have too many #{model}!\nPlease deactivate and try again #{edit_url[1]}")
+  event.message.delete
+end
+
+def unknown_member(event)
+  content = event.message.content
+  content += "\n\n **_I DONT KNOW THIS APPLICANT_**"
+
+  event.message.delete
+  event.respond(content)
+end
+
+def reject_app(event, embed)
+  content = event.message.content
+  event.message.delete
+  reject = event.send_embed(content, embed)
+
+  Emoji::APP_SECTIONS.each do |reaction|
+    reject.react(reaction)
+  end
+
+  reject.react(Emoji::CHECK)
+  reject.react(Emoji::CROSS)
+  reject.react(Emoji::CRAYON)
+end

+ 39 - 0
app/responses/char_display.rb

@@ -0,0 +1,39 @@
+def character_embed(character, image, member)
+  fields = []
+  user = "#{member.name}##{member.tag}"
+  footer_text = "Created by #{user} | #{character.active}"
+  footer_text += " | #{character.rating}" if character.rating
+
+  fields.push({name: 'Species', value: character.species, inline: true}) if character.species
+  fields.push({name: 'Type', value: character.types, inline: true}) if character.types
+  fields.push({name: 'Age', value: character.age, inline: true}) if character.age
+  fields.push({name: 'Weight', value: character.weight, inline: true}) if character.weight
+  fields.push({name: 'Height', value: character.height, inline: true}) if character.height
+  fields.push({name: 'Gender', value: character.gender, inline: true}) if character.gender
+  fields.push({name: 'Sexual Orientation', value: character.orientation, inline: true}) if character.orientation
+  fields.push({name: 'Relationship Status', value: character.relationship, inline: true}) if character.relationship
+  fields.push({name: 'Hometown', value: character.hometown, inline: true}) if character.hometown
+  fields.push({name: 'Location', value: character.location, inline: true}) if character.location
+  fields.push({name: 'Attacks', value: character.attacks}) if character.attacks
+  fields.push({name: 'Likes', value: character.likes}) if character.likes
+  fields.push({name: 'Dislikes', value: character.dislikes}) if character.dislikes
+  fields.push({name: 'Warnings', value: character.warnings}) if character.warnings
+  fields.push({name: 'Rumors', value: character.rumors}) if character.rumors
+  fields.push({name: 'Backstory', value: character.backstory}) if character.backstory
+  fields.push({name: 'Other', value: character.other}) if character.other
+  fields.push({name: 'DM Notes', value: character.dm_notes}) if character.dm_notes
+
+  embed = Embed.new(
+    footer: {
+      text: footer_text
+    },
+    title: character.name,
+    fields: fields
+  )
+
+  embed.description = character.personality if character.personality
+  embed.thumbnail = { url: image } if image
+  embed.color = member.color.combined if member.color.combined
+
+  embed
+end

+ 57 - 0
app/responses/reject.rb

@@ -0,0 +1,57 @@
+def reject_char_embed(app)
+  image_url = /\*\*URL to the Character\'s Appearance\*\*\:\s(.*)/.match(app)
+
+  fields = []
+
+  CharAppResponses::REJECT_MESSAGES.map do |emoji, message|
+    fields.push({ name: emoji, value: "#{message}\n#{CharAppResponses::INLINE_SPACE}", inline: true })
+  end
+
+  fields.push({ name: "Submitting", value: "#{Emoji::CHECK} : Indicates you are ready to send the corresponding messages to the user\n#{Emoji::CROSS} : Indicates you want to dismiss this message and not send a message to the user\n#{Emoji::CRAYON} : Indicates you want to edit the users form for them, and resubmit on their behalf" })
+
+  Embed.new(
+    title: "**_APPLICATION REJECTED_**",
+    description: "Please indicate what message to forward to the user!",
+    color: "#a41e1f",
+    thumbnail: {
+      url: image_url[1]
+    },
+    fields: fields
+  )
+end
+
+def message_user_embed(event)
+  reactions = event.message.reactions
+  content = event.message.content
+
+  edit_url = EDIT_URL.match(content)
+  description = ""
+
+  Emoji::APP_SECTIONS.each do |reaction|
+    if reactions[reaction].count > 1
+      m = CharAppResponses::REJECT_MESSAGES[reaction].gsub("\n", " ")
+      description += "\n#{m}"
+    end
+  end
+
+  embed = Embed.new(
+    title: "**Your application has been rejected!!**",
+    color: "#a41e1f",
+    fields: [
+      { name: "Listed reasons for rejection:", value: description },
+      { name: "You can edit your application and resubmit here:", value: "#{APP_FORM}#{edit_url[1]}" }
+    ]
+  )
+
+  embed
+end
+
+def self_edit_embed(content)
+  edit_url = EDIT_URL.match(content)
+
+  Embed.new(
+    title: "Please edit the user's application and resubmit!",
+    color: "#a41e1f",
+    description: "#{APP_FORM}#{edit_url[1]}"
+  )
+end

+ 106 - 3
bot.rb

@@ -11,8 +11,25 @@ require 'active_record'
 
 # Constants: such as roles and channel ids
 
+# Users
+APP_BOT = 627702340018896896
+
+# Roles
+ADMINS = 308250685554556930
+
+# Channels
+CHAR_CHANNEL = 594244240020865035
+
+# Images
 HAP_ROTOM = "https://static.pokemonpets.com/images/monsters-images-800-800/479-Rotom.png"
 
+# URLs
+APP_FORM = "https://docs.google.com/forms/d/e/1FAIpQLSfryXixX3aKBNQxZT8xOfWzuF02emkJbqJ1mbMGxZkwCvsjyA/viewform"
+
+# Regexes
+UID = /<@([0-9]+)>/
+EDIT_URL = /Edit\sKey\s\(ignore\):\s([\s\S]*)/
+
 # ---
 
 Dotenv.load if BOT_ENV != 'production'
@@ -32,6 +49,7 @@ ActiveRecord::Base.establish_connection(
 )
 
 Dir['app/**/*.rb'].each { |f| require File.join(File.expand_path(__dir__), f) }
+Dir['/lib/*.rb'].each { |f| require f }
 
 
 token = ENV['DISCORD_BOT_TOKEN']
@@ -73,11 +91,31 @@ matchup = Command.new(:matchup) do |event, type|
   end
 end
 
+app = Command.new(:app) do |event, name|
+  user = event.author
+  user_channel = event.author.dm
+
+  if name
+    if character = Character.where(user_id: user.id).find_by(name: name)
+      edit_url = APP_FORM + character.edit_url
+      embed = edit_app_embed(event, edit_url, name)
+
+      bot.send_message(user_channel.id, "", false, embed)
+    else
+      app_not_found_embed(event, name)
+    end
+  else
+    embed = new_app_embed(event)
+    bot.send_message(user_channel.id, "", false, embed)
+  end
+end
+
 # ---
 
 commands = [
   hello,
-  matchup
+  matchup,
+  app
 ]
 
 # This will trigger on every message sent in discord
@@ -98,15 +136,80 @@ bot.message do |event|
           event.respond("Something went wrong!")
         end
       end
+  end
 
+  if event.author.id == APP_BOT
+    Character.check_user(event)
   end
+
+end
+
+# This will trigger when a dm is sent to the bot from a user
+bot.pm do |event|
 end
 
-# This will trigger on every reaction is added in discord
+# This will trigger when any reaction is added in discord
 bot.reaction_add do |event|
+  content = event.message.content
+
+  if event.message.author.id == APP_BOT
+    maj = event.server.roles.find{ |r| r.id == ADMINS }.members.count / 2
+    maj = 1
+
+    if event.message.reacted_with(Emoji::Y).count > maj
+      params = content.split("\n")
+      uid = UID.match(content)
+      member = event.server.member(uid[1])
+
+      character = CharacterController.edit_character(params)
+      image_url = ImageController.edit_images(content, character.id)
+
+      embed = character_embed(character, image_url, member)
+
+      if embed
+        event.message.delete
+
+        bot.send_message(
+          CHAR_CHANNEL,
+          "Character Approved!",
+          false,
+          embed
+        )
+      else
+        event.respond("Something went wrong")
+      end
+
+    elsif event.message.reacted_with(Emoji::N).count > maj
+      embed = reject_char_embed(content)
+      reject_app(event, embed)
+    end
+  end
+
+  if event.message.from_bot? && content.match(/\_New\sCharacter\sApplication\_/)
+    if event.message.reacted_with(Emoji::CHECK).count > 1
+      user_id = UID.match(content)
+      member = event.server.member(user_id[1])
+
+      embed = message_user_embed(event)
+
+      event.message.delete
+      bot.send_temporary_message(event.channel.id, "", 5, false, embed)
+
+      user_channel = member.dm
+      bot.send_message(user_channel.id, "", false, embed)
+
+    elsif event.message.reacted_with(Emoji::CROSS).count > 1
+      event.message.delete
+    elsif event.message.reacted_with(Emoji::CRAYON).count > 1
+      embed = self_edit_embed(content)
+
+      event.message.delete
+      bot.send_temporary_message(event.channel.id, "", 35, false, embed)
+    end
+  end
 end
 
-# This will trigger on every reaction is removed in discord
+# This will trigger when any reaction is removed in discord
 bot.reaction_remove do |event|
 end
 

+ 225 - 0
db/schema.sql

@@ -0,0 +1,225 @@
+--
+-- PostgreSQL database dump
+--
+
+-- Dumped from database version 11.5
+-- Dumped by pg_dump version 11.5
+
+SET statement_timeout = 0;
+SET lock_timeout = 0;
+SET idle_in_transaction_session_timeout = 0;
+SET client_encoding = 'UTF8';
+SET standard_conforming_strings = on;
+SELECT pg_catalog.set_config('search_path', '', false);
+SET check_function_bodies = false;
+SET xmloption = content;
+SET client_min_messages = warning;
+SET row_security = off;
+
+SET default_tablespace = '';
+
+SET default_with_oids = false;
+
+--
+-- Name: char_images; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.char_images (
+    id integer NOT NULL,
+    char_id integer NOT NULL,
+    url character varying NOT NULL,
+    category character varying,
+    keyword character varying
+);
+
+
+--
+-- Name: char_images_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.char_images_id_seq
+    AS integer
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+--
+-- Name: char_images_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.char_images_id_seq OWNED BY public.char_images.id;
+
+
+--
+-- Name: characters; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.characters (
+    id integer NOT NULL,
+    user_id character varying(50),
+    name character varying NOT NULL,
+    species character varying NOT NULL,
+    types character varying NOT NULL,
+    age character varying,
+    weight character varying,
+    height character varying,
+    gender character varying,
+    orientation character varying,
+    relationship character varying,
+    attacks character varying,
+    likes character varying,
+    dislikes character varying,
+    personality character varying,
+    backstory character varying,
+    other character varying,
+    edit_url character varying,
+    active character varying,
+    dm_notes character varying,
+    location character varying,
+    rumors character varying
+);
+
+
+--
+-- Name: characters_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.characters_id_seq
+    AS integer
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+--
+-- Name: characters_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.characters_id_seq OWNED BY public.characters.id;
+
+
+--
+-- Name: users; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.users (
+    id character varying(25) NOT NULL,
+    level integer DEFAULT 1,
+    next_level integer DEFAULT 22,
+    boosted_xp integer DEFAULT 0,
+    unboosted_xp integer DEFAULT 0,
+    evs character varying(50) NOT NULL,
+    hp integer NOT NULL,
+    attack integer NOT NULL,
+    defense integer NOT NULL,
+    sp_attack integer NOT NULL,
+    sp_defense integer NOT NULL,
+    speed integer NOT NULL
+);
+
+
+--
+-- Name: char_images id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.char_images ALTER COLUMN id SET DEFAULT nextval('public.char_images_id_seq'::regclass);
+
+
+--
+-- Name: characters id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.characters ALTER COLUMN id SET DEFAULT nextval('public.characters_id_seq'::regclass);
+
+
+--
+-- Data for Name: char_images; Type: TABLE DATA; Schema: public; Owner: -
+--
+
+COPY public.char_images (id, char_id, url, category, keyword) FROM stdin;
+\.
+
+
+--
+-- Data for Name: characters; Type: TABLE DATA; Schema: public; Owner: -
+--
+
+COPY public.characters (id, user_id, name, species, types, age, weight, height, gender, orientation, relationship, attacks, likes, dislikes, personality, backstory, other, edit_url, active, dm_notes, location, rumors) FROM stdin;
+1	215240568245190656	Mizukyu	Mimikyu	Ghost Fairy	old	1.5 lbs	0'8"	Female	Pansexual	Married	Shadow Claw | Play Rough | Psychic | Shadow Sneak	cuddles, soft things, and Neiro waterbed	bullies, rejection, being exposed	I am shy and a bit recluse, but warm when you take the time to get to know me. I don't like when people try to see under my disguise, I've lost many friends that way (to death) including my spouse. Really, really really likes to dress up as other pokemon	Has existed more years than she cares to count. She is immortal, due to being a ghost. She has learned to carefully hide her appearance as to make sure not to accidentally kill more friends. Took up tailoring to make multiple disguises, because pretending to be a Pikachu forever is boring.	Switches disguising with moods. Has made them for all pokemon. Also a master shadow bender	?edit2=2_ABaOnuc3HZEyhI9EeApXcJtsBmDzqtzDGH5De46CfuRBxwVavQKAfTT_LZy_kMH0sz5H7gk	Active	\N	\N	loves children | hates washing and tailoring disguises | surprisingly soft| steals children | wishes to be admired
+\.
+
+
+--
+-- Data for Name: users; Type: TABLE DATA; Schema: public; Owner: -
+--
+
+COPY public.users (id, level, next_level, boosted_xp, unboosted_xp, evs, hp, attack, defense, sp_attack, sp_defense, speed) FROM stdin;
+271741998321369088	70	25550	25285	18368	24, 4, 5, 5, 6, 5	432	127	128	143	145	136
+215240568245190656	30	4950	4686	984	23, 4, 6, 6, 4, 5	201	67	67	69	62	64
+263878163975634947	17	1702	1675	1227	19, 5, 4, 5, 6, 4	113	39	41	39	42	38
+\.
+
+
+--
+-- Name: char_images_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
+--
+
+SELECT pg_catalog.setval('public.char_images_id_seq', 1, false);
+
+
+--
+-- Name: characters_id_seq; Type: SEQUENCE SET; Schema: public; Owner: -
+--
+
+SELECT pg_catalog.setval('public.characters_id_seq', 1, true);
+
+
+--
+-- Name: char_images char_images_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.char_images
+    ADD CONSTRAINT char_images_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: characters characters_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.characters
+    ADD CONSTRAINT characters_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: users users_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.users
+    ADD CONSTRAINT users_pkey PRIMARY KEY (id);
+
+
+--
+-- Name: characters character_user_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.characters
+    ADD CONSTRAINT character_user_id FOREIGN KEY (user_id) REFERENCES public.users(id);
+
+
+--
+-- Name: char_images url_character_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.char_images
+    ADD CONSTRAINT url_character_id FOREIGN KEY (char_id) REFERENCES public.characters(id);
+
+
+--
+-- PostgreSQL database dump complete
+--
+

+ 44 - 0
lib/emoji.rb

@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Emoji
+  A = "🇦"
+  B = "🇧"
+  C = "🇨"
+  D = "🇩"
+  E = "🇪"
+  F = "🇫"
+  G = "🇬"
+  H = "🇭"
+  I = "🇮"
+  J = "🇯"
+  K = "🇰"
+  L = "🇱"
+  M = "🇲"
+  N = "🇳"
+  O = "🇴"
+  P = "🇵"
+  Q = "🇶"
+  R = "🇷"
+  S = "🇸"
+  T = "🇹"
+  U = "🇺"
+  V = "🇻"
+  W = "🇼"
+  X = "🇽"
+  Y = "🇾"
+  Z = "🇿"
+  CHECK = "✅"
+  CROSS = "❌"
+  SPEECH_BUBBLE = "💬"
+  SCALE = "⚖"
+  PICTURE = "🖼"
+  BOOKS = "📚"
+  NOTE = "🗒"
+  SKULL = "☠"
+  BABY = "👶"
+  VULGAR = "🖕"
+  CRAYON = "🖍"
+
+  LETTERS = [A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
+  APP_SECTIONS = [SPEECH_BUBBLE, SCALE, PICTURE, BOOKS, BABY, SKULL, VULGAR, NOTE]
+end