Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Init 2.0 skeleton (starting from db service) + 9armbot console #47

Merged
merged 12 commits into from
Apr 20, 2021
60 changes: 34 additions & 26 deletions .github/workflows/pull-test.yml
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
name: PR unit test

on:
pull_request:
paths:
- '9armbot/**'
- 'core/**'
- '__test__/**'
branches:
- main

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: '14'
architecture: 'x64'
check-latest: true
- name: Install dependencies
run: npm install
- name: Test
run: npm test
name: PR unit test

on:
push:
branches:
- main
- v2.0.0
pull_request:
paths:
- "9armbot/**"
- "9armbot-2.0/**"
- "core/**"
- "__test__/**"
branches:
- main
- v2.0.0

jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: "14"
architecture: "x64"
check-latest: true
- name: Install dependencies
run: npm install
- name: Test
run: npm test
- name: Test (2.0)
run: npm run test-2.0
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ node_modules
9armbot/players.json
package-lock.json
yarn.lock
players-2.0.json
.node_repl_history
dist
6 changes: 6 additions & 0 deletions 9armbot-2.0/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all"
}
28 changes: 28 additions & 0 deletions 9armbot-2.0/console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* 9armbot Console
* Warning : Since this console toes not share the same process as the NodeJS server,
* writing to db will overwrite everything! (Singleton does not work across processes, damn!)
*/

import repl from 'repl'
import _ from 'lodash'
import { dbService } from './services/db'

const replServer = repl.start({
prompt: `9armbot(${process.env.NODE_ENV || 'development'}) > `,
})

replServer.setupHistory('./.node_repl_history', () => {})

// Preload database
dbService.load()

console.log('Database loaded, press enter to continue.')

// Access db eg. `db.read()`
// Type `db.` then press Tab to see all available commands
replServer.context.db = dbService

// Lodash (_ is reserved, use l or __ instead)
replServer.context.l = _
replServer.context.__ = _
19 changes: 19 additions & 0 deletions 9armbot-2.0/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { twitchService } from './services/twitch'
import { discordService } from './services/discord'
import { dbService } from './services/db'

async function main() {
dbService.load()
dbService.save()

await twitchService()
await discordService()

console.log('9armbot 2.0 Running...')

setInterval(() => {
console.log('db', 'save database', dbService.save())
}, 15000)
}

main()
5 changes: 5 additions & 0 deletions 9armbot-2.0/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testMatch: ["**/test/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[tj]s?(x)"],
};
1 change: 1 addition & 0 deletions 9armbot-2.0/services/bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export class Bot {}
88 changes: 88 additions & 0 deletions 9armbot-2.0/services/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import find from 'lodash/find'
import fs from 'fs'
import { nanoid } from 'nanoid'
import path from 'path'

const DB_PATH = path.resolve(process.cwd(), './players-2.0.json')

export interface IDb {
players: IPlayer[]
}

export interface IPlayer {
uid: string
username: string
}

export class Db {
private db: IDb = {
players: [],
}

public load(): void {
try {
const playersJson = fs.readFileSync(DB_PATH, 'utf8')
this.db = JSON.parse(playersJson)
console.log(`Loaded ${this.db.players.length} players from ${DB_PATH}.`)
} catch (err) {
console.log('[ERROR] File not found, use default blank db.', err.message)
}
}

public save(): void {
try {
const data = JSON.stringify(this.read(), null, 2)
fs.writeFileSync(DB_PATH, data, 'utf8')
console.log(`Saved ${this.db.players.length} players to ${DB_PATH}.`)
} catch (err) {
console.log('[ERROR] Cannot write file.', err.message)
}
}

public read(): IDb {
return this.db
}

public createPlayer(username: string): IPlayer {
// Don't re-create existing player
const player = this.getPlayerbyUsername(username)

if (player) {
return player
}

const newPlayer: IPlayer = {
uid: nanoid(),
username,
}

this.db.players.push(newPlayer)

return newPlayer
}

public getPlayerbyUsername(username: string): IPlayer | undefined {
return find(this.db.players, (p) => {
return p.username === username
})
}
}

// Graceful Shutdown
function gracefulShutdown(): void {
console.log('\nPre-Close')
dbService.save()
setTimeout(() => {
console.log('Closed')
process.exit(0)
}, 1000)
}
process.on('SIGINT', () => gracefulShutdown())
process.on('SIGTERM', () => gracefulShutdown())
process.on('uncaughtException', (err) => {
console.error(err)
gracefulShutdown()
})

// Use singleton pattern to mitigate multiple readers/writers
export const dbService = new Db()
23 changes: 23 additions & 0 deletions 9armbot-2.0/services/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import dotenv from 'dotenv'
import { customAlphabet } from 'nanoid'
import { dbService } from './db'

dotenv.config()

export async function discordService() {
// Test db reading
setInterval(() => {
console.log('discord read db from dbservice', dbService.read())
}, 2500)

// Test db writing
const randomName = customAlphabet('1234567890abcdefghijklmnopqrstuvwxyz', 8)

setInterval(() => {
console.log(
'discord',
'test adding random players',
dbService.createPlayer(randomName()),
)
}, 7000)
}
31 changes: 31 additions & 0 deletions 9armbot-2.0/services/twitch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import tmi from 'tmi.js'
import fs from 'fs'
import dotenv from 'dotenv'
import path from 'path'
import { dbService } from './db'

dotenv.config()

let oauth_token = fs.readFileSync(
path.resolve(__dirname, '../../9armbot/oauth_token'),
'utf8',
)

export async function twitchService() {
const client = new tmi.Client({
options: { debug: true },
connection: { reconnect: true },
identity: {
username: process.env.tmi_username,
password: oauth_token,
},
channels: [process.env.tmi_channel_name as string],
})

await client.connect()

// Test db reading
setInterval(() => {
console.log('twitch read db from dbservice', dbService.read())
}, 2000)
}
113 changes: 113 additions & 0 deletions 9armbot-2.0/test/db.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import fs from 'fs'
import { Db, IDb } from '../services/db'

jest.mock('fs')

const mockFs = fs as jest.Mocked<typeof fs>

let db: Db

beforeEach(() => {
db = new Db()
})

describe('Database', () => {
describe('#load', () => {
it('loads data from json file and initiate players', () => {
const dbFileContent: IDb = {
players: [
{
uid: 'uid',
username: 'foo',
},
],
}

mockFs.readFileSync.mockReturnValueOnce(JSON.stringify(dbFileContent))

db.load()

expect(db.read()).toEqual(dbFileContent)
expect(mockFs.readFileSync).toHaveBeenCalledTimes(1)
})

it('falls back to default if file does not exist', () => {
const dbFileBlank: IDb = {
players: [],
}

mockFs.readFileSync.mockImplementation(() => {
throw new Error('ENOENT: no such file or directory')
})

expect(db.read()).toEqual(dbFileBlank)
expect(mockFs.readFileSync).toHaveBeenCalledTimes(1)
})
})

describe('#save', () => {
it('saves the current database state to json file', () => {
db.createPlayer('foo')

const expectedJson = JSON.stringify(db.read(), null, 2)

db.save()

expect(mockFs.writeFileSync).toHaveBeenCalledTimes(1)
expect(mockFs.writeFileSync).toHaveBeenCalledWith(
expect.any(String),
expectedJson,
'utf8',
)
})
})
})

describe('CRUD players', () => {
describe('#createPlayer', () => {
it('can create new player by username (twitch for now)', () => {
db.createPlayer('foo')

expect(db.read()).toEqual({
players: [
{
uid: expect.any(String),
username: 'foo',
},
],
})
})

it('does not create new player if username is existed', () => {
db.createPlayer('foo')
db.createPlayer('foo')
db.createPlayer('foo')

expect(db.read()).toEqual({
players: [
{
uid: expect.any(String),
username: 'foo',
},
],
})
})
})

describe('#getPlayerbyUsername', () => {
it('returns undefined if player is not found', () => {
expect(db.getPlayerbyUsername('foo')).toBeUndefined
})

it('returns player if found by username', () => {
db.createPlayer('foo')

expect(db.getPlayerbyUsername('foo')).toEqual({
uid: expect.any(String),
username: 'foo',
})

expect(db.getPlayerbyUsername('bar')).toBeUndefined
})
})
})
Loading