├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── Procfile ├── roles.config.js ├── assets ├── SS1.png ├── SS2.png ├── SS3.png ├── SS4.png ├── SS5.png ├── SS6.png └── SS7.png ├── .sample.env ├── example ├── GURU.xlsx └── X BAHASA.xlsx ├── commitlint.config.cjs ├── .gitignore ├── validator ├── authorization.js └── argumentsValidator.js ├── .eslintrc.cjs ├── app.json ├── models ├── class.js ├── absen.js └── user.js ├── utils ├── regex.js ├── whatsRole.js ├── date.js ├── permittedOrNot.js └── readALotXLSX.js ├── handler ├── database.js └── message.js ├── common ├── availableClass.js ├── initialize.js ├── teacherSorter.js ├── studentSorter.js └── message.js ├── routes └── message.js ├── LICENSE ├── index.js ├── package.json ├── .github └── workflows │ └── lint.yml ├── controller └── message.js └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | worker: node index.js 2 | -------------------------------------------------------------------------------- /roles.config.js: -------------------------------------------------------------------------------- 1 | export default ["admin", "guru", "pengurus", "siswa"]; 2 | -------------------------------------------------------------------------------- /assets/SS1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/assets/SS1.png -------------------------------------------------------------------------------- /assets/SS2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/assets/SS2.png -------------------------------------------------------------------------------- /assets/SS3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/assets/SS3.png -------------------------------------------------------------------------------- /assets/SS4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/assets/SS4.png -------------------------------------------------------------------------------- /assets/SS5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/assets/SS5.png -------------------------------------------------------------------------------- /assets/SS6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/assets/SS6.png -------------------------------------------------------------------------------- /assets/SS7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/assets/SS7.png -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /.sample.env: -------------------------------------------------------------------------------- 1 | PREFIX=N# 2 | MONGO_DB_CONNECTION_STRING=mongodb://127.0.0.1:27017/nesbaen 3 | -------------------------------------------------------------------------------- /example/GURU.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/example/GURU.xlsx -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /example/X BAHASA.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reacto11mecha/nesbaen/HEAD/example/X BAHASA.xlsx -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --config commitlint.config.cjs --edit "" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | logs/ 3 | .env 4 | sensitive/ 5 | # managed by open-wa 6 | **.data.json 7 | **.node-persist** 8 | # end managed by open-wa 9 | 10 | # managed by open-wa 11 | **_IGNORE_** 12 | # end managed by open-wa 13 | 14 | # eslint stuff 15 | eslint_report.json 16 | .eslintcache -------------------------------------------------------------------------------- /validator/authorization.js: -------------------------------------------------------------------------------- 1 | import permittedOrNot from "../utils/permittedOrNot.js"; 2 | 3 | const isStudent = permittedOrNot("siswa", "pengurus"); 4 | const isManager = permittedOrNot("pengurus", "guru", "admin"); 5 | const isAdmin = permittedOrNot("admin"); 6 | 7 | export { isStudent, isManager, isAdmin }; 8 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "root": true, 3 | "env": { 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["eslint:recommended", "prettier"], 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nesbaen", 3 | "description": "nesbaen", 4 | "repository": "https://github.com/reacto11mecha/nesbaen", 5 | "author": "Ezra Khairan Permana", 6 | "keywords": ["nodejs", "bot", "whatsapp bot", "whatsapp-bot-nodejs", "nodejs-bot", "javascript", "whatsapp automation"], 7 | "buildpacks": [{ 8 | "url": "heroku/nodejs" 9 | }] 10 | } 11 | -------------------------------------------------------------------------------- /models/class.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | 3 | const classNames = new mongoose.Schema({ 4 | name: { 5 | type: String, 6 | required: true, 7 | }, 8 | }); 9 | 10 | const Class = new mongoose.Schema({ 11 | gradeName: { type: String, required: true }, 12 | classNames: [classNames], 13 | }); 14 | 15 | export default mongoose.model("Class", Class); 16 | -------------------------------------------------------------------------------- /utils/regex.js: -------------------------------------------------------------------------------- 1 | const EmailRegex = 2 | /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 3 | const NameRegex = 4 | /^(([A-Za-z]+[\-\']?)*([A-Za-z]+)?\s)+([A-Za-z]+[\-\']?)*([A-Za-z]+)?$/; // eslint-disable-line 5 | const xlsxSheetSplitter = /(?<=^\S+)\s/; 6 | 7 | export { EmailRegex, NameRegex, xlsxSheetSplitter }; 8 | -------------------------------------------------------------------------------- /handler/database.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import dotenv from "dotenv"; 3 | 4 | import initialize from "../common/initialize.js"; 5 | 6 | dotenv.config(); 7 | 8 | mongoose 9 | .connect(process.env.MONGO_DB_CONNECTION_STRING, { 10 | useNewUrlParser: true, 11 | useUnifiedTopology: true, 12 | }) 13 | .then(async () => { 14 | console.log("DB CONNECTED"); 15 | await initialize(); 16 | }) 17 | .catch((err) => { 18 | console.log(err); 19 | process.exit(); 20 | }); 21 | -------------------------------------------------------------------------------- /utils/whatsRole.js: -------------------------------------------------------------------------------- 1 | import roles from "../roles.config.js"; 2 | 3 | const whatsRole = (stringRole) => { 4 | const trimmed = stringRole.toLowerCase().trim(); 5 | 6 | if (!trimmed.includes(",")) { 7 | if (!roles.includes(trimmed)) 8 | return new Error(`Role ${trimmed} tidak ada!`); 9 | 10 | return [trimmed]; 11 | } 12 | 13 | return trimmed.split(",").map((role) => { 14 | const trimmedRole = role.trim(); 15 | 16 | if (!roles.includes(trimmedRole)) 17 | return new Error(`Role ${trimmed} tidak ada!`); 18 | 19 | return trimmedRole; 20 | }); 21 | }; 22 | 23 | export default whatsRole; 24 | -------------------------------------------------------------------------------- /common/availableClass.js: -------------------------------------------------------------------------------- 1 | const SUBJECTS = ["IPA", "IPS", "BAHASA"]; 2 | 3 | const createGradeClass = (length, subjectIDX) => { 4 | if (length > 1) { 5 | const dummyArray = Array.from(new Array(length)); 6 | 7 | return dummyArray.map((_, idx) => ({ 8 | name: `${SUBJECTS[subjectIDX]} ${++idx}`, 9 | })); 10 | } 11 | 12 | return { name: `${SUBJECTS[subjectIDX]}` }; 13 | }; 14 | 15 | const currentAvailableClass = [ 16 | { 17 | gradeName: "X", 18 | classNames: [ 19 | ...createGradeClass(6, 0), 20 | ...createGradeClass(4, 1), 21 | createGradeClass(1, 2), 22 | ], 23 | }, 24 | ]; 25 | 26 | export default currentAvailableClass; 27 | export { SUBJECTS, createGradeClass }; 28 | -------------------------------------------------------------------------------- /models/absen.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | 4 | const userListSchema = new mongoose.Schema({ 5 | user_id: { 6 | type: mongoose.Schema.Types.ObjectId, 7 | ref: "User", 8 | }, 9 | precense_time: { 10 | type: Date, 11 | default: Date.now, 12 | }, 13 | }); 14 | 15 | const Absen = new mongoose.Schema({ 16 | absen_id: { 17 | type: String, 18 | default: uuidv4, 19 | }, 20 | assignator: { 21 | type: mongoose.Schema.Types.ObjectId, 22 | ref: "User", 23 | required: true, 24 | }, 25 | userList: [userListSchema], 26 | created_at: { 27 | type: Date, 28 | default: Date.now, 29 | }, 30 | }); 31 | 32 | export default mongoose.model("Absen", Absen); 33 | -------------------------------------------------------------------------------- /routes/message.js: -------------------------------------------------------------------------------- 1 | import EventEmitter from "events"; 2 | 3 | import { isStudent, isManager } from "../validator/authorization.js"; 4 | 5 | import { helpReply } from "../common/message.js"; 6 | import controller from "../controller/message.js"; 7 | 8 | export default function initEmitter() { 9 | const messageHandler = new EventEmitter(); 10 | 11 | messageHandler.on("me", controller.me); 12 | 13 | messageHandler.on("absen", isStudent(controller.absen)); 14 | messageHandler.on("generate", isManager(controller.generate)); 15 | messageHandler.on("list-created", isManager(controller.listCreated)); 16 | messageHandler.on("lists", isManager(controller.lists)); 17 | messageHandler.on("delete", isManager(controller.delete)); 18 | 19 | messageHandler.on( 20 | "help", 21 | async ({ client, message }) => 22 | await client.reply(message.from, helpReply, message.id, true) 23 | ); 24 | 25 | return messageHandler; 26 | } 27 | -------------------------------------------------------------------------------- /utils/date.js: -------------------------------------------------------------------------------- 1 | const konversiBulan = (number) => { 2 | switch (number) { 3 | case 0: 4 | return "Januari"; 5 | case 1: 6 | return "Februari"; 7 | case 2: 8 | return "Maret"; 9 | case 3: 10 | return "April"; 11 | case 4: 12 | return "Mei"; 13 | case 5: 14 | return "Juni"; 15 | case 6: 16 | return "Juli"; 17 | case 7: 18 | return "Agustus"; 19 | case 8: 20 | return "September"; 21 | case 9: 22 | return "Oktober"; 23 | case 10: 24 | return "November"; 25 | case 11: 26 | return "Desember"; 27 | default: 28 | break; 29 | } 30 | }; 31 | 32 | const updateTime = (t) => (t < 10 ? "0" + t : t); 33 | 34 | const generateDateString = (time) => 35 | `${time.getDate()} ${konversiBulan( 36 | time.getMonth() 37 | )} ${time.getFullYear()} ${updateTime(time.getHours())}:${updateTime( 38 | time.getMinutes() 39 | )}:${updateTime(time.getSeconds())}`; 40 | 41 | export { konversiBulan, updateTime, generateDateString }; 42 | -------------------------------------------------------------------------------- /validator/argumentsValidator.js: -------------------------------------------------------------------------------- 1 | import { version as uuidVersion, validate as uuidValidate } from "uuid"; 2 | 3 | const isUUID4 = async ({ args, client, message }) => { 4 | const haveArguments = await isArgumentExist({ args, client, message }); 5 | 6 | if (!haveArguments) return haveArguments; 7 | 8 | const isValidUUID4 = uuidValidate(args[0]) && uuidVersion(args[0]) === 4; 9 | 10 | if (!isValidUUID4) { 11 | await client.simulateTyping(message.from, false); 12 | await client.reply( 13 | message.from, 14 | "Id absen yang dimasukkan tidak valid !", 15 | message.id, 16 | true 17 | ); 18 | return false; 19 | } 20 | 21 | return isValidUUID4; 22 | }; 23 | 24 | const isArgumentExist = async ({ args, client, message }) => { 25 | if (args.length === 0) { 26 | await client.reply( 27 | message.from, 28 | "Argumen dibutuhkan setelah perintah !", 29 | message.id, 30 | true 31 | ); 32 | return false; 33 | } 34 | 35 | return true; 36 | }; 37 | 38 | export { isUUID4, isArgumentExist }; 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ezra Khairan Permana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /utils/permittedOrNot.js: -------------------------------------------------------------------------------- 1 | import User from "../models/user.js"; 2 | 3 | const permittedOrNot = (...permittedRole) => (callback) => async (miscData) => { 4 | const { client, message, userNumber } = miscData; 5 | const user = await checkUser({ client, message, userNumber }); 6 | 7 | if (!user) return false; 8 | 9 | const isPermitted = permittedRole.some((role) => user.roles.includes(role)); 10 | 11 | if (!isPermitted) { 12 | await client.reply( 13 | message.from, 14 | "Anda tidak diizinkan menggunakan perintah tersebut !", 15 | message.id, 16 | true 17 | ); 18 | return false; 19 | } 20 | 21 | return await callback({ ...miscData, user }); 22 | }; 23 | 24 | const checkUser = async ({ userNumber, client, message }) => { 25 | const user = await getUser(userNumber); 26 | 27 | if (!user) { 28 | await client.simulateTyping(message.from, false); 29 | await client.reply( 30 | message.from, 31 | "Anda belum terdaftar !", 32 | message.id, 33 | true 34 | ); 35 | return false; 36 | } 37 | 38 | return user; 39 | }; 40 | 41 | const getUser = async (phoneNumber) => 42 | await User.findOne({ phoneNumber }).lean(); 43 | 44 | export default permittedOrNot; 45 | export { checkUser, getUser }; 46 | -------------------------------------------------------------------------------- /common/initialize.js: -------------------------------------------------------------------------------- 1 | import User from "../models/user.js"; 2 | import Class from "../models/class.js"; 3 | import getUsersData from "../utils/readALotXLSX.js"; 4 | import currentAvailableClass from "./availableClass.js"; 5 | 6 | const proceedUser = async () => 7 | await User.find().then(async (data) => { 8 | if (data.length === 0) { 9 | const allUsers = await getUsersData(); 10 | 11 | User.insertMany(allUsers) 12 | .then(() => console.log("[DB] User ditambahkan")) 13 | .catch((e) => { 14 | console.error(`Error: ${e}`); 15 | process.exit(); 16 | }); 17 | } 18 | }); 19 | 20 | const proceedClass = async () => 21 | await Class.find().then(async (data) => { 22 | if (data.length === 0) { 23 | for (const currentClass of currentAvailableClass) { 24 | const newGrade = new Class({ ...currentClass }); 25 | newGrade.classNames = currentClass.classNames; 26 | 27 | await newGrade.save(); 28 | } 29 | 30 | console.log("[DB] Kelas ditambahkan"); 31 | } 32 | }); 33 | 34 | const initialize = async () => { 35 | await proceedClass(); 36 | await proceedUser(); 37 | console.log("[DB] Prosedur inisialisasi selesai"); 38 | }; 39 | 40 | export default initialize; 41 | export { proceedUser, proceedClass }; 42 | -------------------------------------------------------------------------------- /common/teacherSorter.js: -------------------------------------------------------------------------------- 1 | import parsePhoneNumber from "libphonenumber-js"; 2 | import { NameRegex } from "../utils/regex.js"; 3 | 4 | const { isPossiblePhoneNumber, isValidPhoneNumber } = parsePhoneNumber; 5 | 6 | const teacherSorter = (reject, file) => (d) => { 7 | const keys = Object.keys(d); 8 | 9 | if (!keys.includes("NAMA GURU") | !keys.includes("NO TELEPON")) 10 | reject( 11 | `Format tidak sesuai. Judul data harus menggunakan "NAMA GURU", "NO TELEPON". File: ${file}` 12 | ); 13 | 14 | const reformatted = { 15 | name: d["NAMA GURU"] 16 | .toLowerCase() 17 | .trim() 18 | .replace(/\b(\w)/g, (s) => s.toUpperCase()), 19 | phoneNumber: d["NO TELEPON"].trim().replace(".", "+"), 20 | roles: ["guru"], 21 | }; 22 | 23 | if ( 24 | !isPossiblePhoneNumber(reformatted.phoneNumber, "ID") && 25 | !isValidPhoneNumber(reformatted.phoneNumber, "ID") 26 | ) 27 | reject( 28 | `Nomor telepon dari guru yang bernama '${reformatted.name}' salah. File: ${file}` 29 | ); 30 | 31 | if (!NameRegex.test(reformatted.name)) 32 | reject( 33 | `Guru bernama ${reformatted.name} namanya salah, coba di cek kembali apakah nama tersebut sudah benar. File: ${file}` 34 | ); 35 | 36 | return reformatted; 37 | }; 38 | 39 | export default teacherSorter; 40 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose"; 2 | import parsePhoneNumber from "libphonenumber-js"; 3 | 4 | import enumRole from "../roles.config.js"; 5 | 6 | import { NameRegex } from "../utils/regex.js"; 7 | 8 | const { isPossiblePhoneNumber, isValidPhoneNumber } = parsePhoneNumber; 9 | 10 | const User = new mongoose.Schema({ 11 | name: { 12 | type: String, 13 | required: [true, "Masukkan nama lengkap!"], 14 | validate: { 15 | validator: (v) => NameRegex.test(v), 16 | message: (props) => `${props.value} bukanlah nama yang valid!`, 17 | }, 18 | }, 19 | roles: [ 20 | { 21 | type: String, 22 | enum: enumRole, 23 | default: "siswa", 24 | }, 25 | ], 26 | phoneNumber: { 27 | type: String, 28 | validate: { 29 | validator: (v) => 30 | isPossiblePhoneNumber(v, "ID") && isValidPhoneNumber(v, "ID"), 31 | message: (props) => `${props.value} bukanlah nama yang valid!`, 32 | }, 33 | }, 34 | className: { 35 | grade: { 36 | type: mongoose.Schema.Types.ObjectId, 37 | ref: "Class", 38 | }, 39 | gradeName: { 40 | type: mongoose.Schema.Types.ObjectId, 41 | ref: "Class.classNames", 42 | }, 43 | }, 44 | created_at: { 45 | type: Date, 46 | default: Date.now, 47 | }, 48 | }); 49 | 50 | export default mongoose.model("User", User); 51 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import { create } from "@open-wa/wa-automate"; 2 | import PQueue from "p-queue"; 3 | import dotenv from "dotenv"; 4 | 5 | dotenv.config(); 6 | 7 | import "./handler/database.js"; 8 | 9 | import handlerProc from "./handler/message.js"; 10 | 11 | let proc; 12 | 13 | const queue = new PQueue({ 14 | concurrency: 4, 15 | autoStart: false, 16 | }); 17 | 18 | const checkValidMessage = (message) => 19 | message.type === "chat" && message.body.startsWith(process.env.PREFIX); 20 | 21 | const processMessage = (message) => { 22 | if (checkValidMessage(message)) { 23 | console.log(`[Pesan] Ada pesan dari : ${message.sender.pushname}`); 24 | queue.add(() => proc(message)); 25 | } 26 | }; 27 | 28 | async function start(client) { 29 | try { 30 | proc = handlerProc(client); 31 | 32 | const unreadMessages = await client.getAllUnreadMessages(); 33 | unreadMessages?.filter(checkValidMessage).forEach(processMessage); 34 | 35 | client.onStateChanged((state) => { 36 | console.log(`[State] Current State: ${state}`); 37 | if (state === "CONFLICT") client.forceRefocus(); 38 | }); 39 | 40 | await client.onMessage(processMessage); 41 | queue.start(); 42 | } catch (error) { 43 | console.error(error); 44 | } 45 | } 46 | 47 | create({ 48 | sessionId: "NESBAEN_BOT", 49 | cacheEnabled: false, 50 | authTimeout: 0, 51 | killProcessOnBrowserClose: true, 52 | qrTimeout: 0, 53 | }).then((client) => start(client)); 54 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nesbaen", 3 | "version": "1.0.0", 4 | "description": "Jangan Lupa Absen ! Ini adalah bot whatsapp untuk absensi", 5 | "exports": "./index.js", 6 | "scripts": { 7 | "start": "node .", 8 | "lint": "eslint .", 9 | "lint:report": "eslint --output-file eslint_report.json --format json .", 10 | "prepare": "husky install" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/reacto11mecha/nesbaen.git" 15 | }, 16 | "author": "Ezra Khairan Permana", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/reacto11mecha/nesbaen/issues" 20 | }, 21 | "homepage": "https://github.com/reacto11mecha/nesbaen#readme", 22 | "dependencies": { 23 | "@open-wa/wa-automate": "^4.30.7", 24 | "dotenv": "^14.3.2", 25 | "libphonenumber-js": "^1.9.46", 26 | "mongoose": "^6.1.8", 27 | "p-queue": "^7.1.0", 28 | "uuid": "^8.3.2", 29 | "xlsx": "^0.17.5" 30 | }, 31 | "type": "module", 32 | "keywords": [ 33 | "nodejs", 34 | "bot", 35 | "mongodb", 36 | "presence", 37 | "presence-management", 38 | "whatsapp-bot" 39 | ], 40 | "devDependencies": { 41 | "@commitlint/cli": "^16.1.0", 42 | "@commitlint/config-conventional": "^16.0.0", 43 | "eslint": "^8.8.0", 44 | "eslint-config-prettier": "^8.3.0", 45 | "husky": "^7.0.4", 46 | "lint-staged": "^12.3.3", 47 | "prettier": "^2.5.1" 48 | }, 49 | "lint-staged": { 50 | "*.js": "eslint --cache --fix", 51 | "*.{js,css,md}": "prettier --write" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: ES Lint 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | push: 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | name: Check if the file passed the linting 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Setup NodeJS 16 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: 16 19 | 20 | - name: Cache node modules 21 | uses: actions/cache@v1 22 | with: 23 | path: ~/.npm 24 | key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} 25 | restore-keys: | 26 | ${{ runner.OS }}-build-${{ env.cache-name }}- 27 | ${{ runner.OS }}-build- 28 | ${{ runner.OS }}- 29 | - name: Install Node Dependencies 30 | run: | 31 | sudo apt-get install libxtst-dev libpng++-dev 32 | npm install 33 | env: 34 | CI: TRUE 35 | 36 | - name: Test Code Linting 37 | run: npm run lint 38 | 39 | - name: Save Code Linting Report JSON 40 | run: npm run lint:report 41 | continue-on-error: true 42 | 43 | - name: Annotate Code Linting Results 44 | uses: ataylorme/eslint-annotate-action@1.2.0 45 | with: 46 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 47 | report-json: "eslint_report.json" 48 | fail-on-warning: true 49 | 50 | - name: Upload ESLint report 51 | uses: actions/upload-artifact@v2 52 | with: 53 | name: eslint_report.json 54 | path: eslint_report.json 55 | -------------------------------------------------------------------------------- /utils/readALotXLSX.js: -------------------------------------------------------------------------------- 1 | import XLSX from "xlsx"; 2 | 3 | import studentSorter from "../common/studentSorter.js"; 4 | import teacherSorter from "../common/teacherSorter.js"; 5 | 6 | import fs from "node:fs"; 7 | import path from "node:path"; 8 | 9 | const sensitiveDIR = new URL("../sensitive", import.meta.url); 10 | const files = fs 11 | .readdirSync(sensitiveDIR) 12 | .filter((file) => file.endsWith(".xlsx")); 13 | 14 | const readXLSX = (file) => 15 | new Promise((resolve, reject) => { 16 | (async () => { 17 | const filePath = path.join(sensitiveDIR.pathname, file); 18 | const sheetName = file.replace(".xlsx", ""); 19 | 20 | const workbook = XLSX.readFile(filePath); 21 | if (!workbook.SheetNames.includes(sheetName)) 22 | reject( 23 | `Format tidak sesuai. Nama sheet harus sama dengan nama file ! File: ${file}` 24 | ); 25 | 26 | const worksheet = workbook.Sheets[sheetName]; 27 | const data = XLSX.utils.sheet_to_json(worksheet, { raw: true }); 28 | 29 | const isTeacher = sheetName === "GURU"; 30 | const sSorter = !isTeacher 31 | ? await studentSorter(sheetName, reject, file) 32 | : null; 33 | 34 | const cleanData = data.map( 35 | isTeacher ? teacherSorter(reject, file) : sSorter 36 | ); 37 | 38 | const sorted = cleanData.sort((a, b) => a.name.localeCompare(b.name)); 39 | 40 | resolve(sorted); 41 | })(); 42 | }); 43 | 44 | export default async function readXLSXes() { 45 | try { 46 | let users = []; 47 | 48 | for (const file of files) { 49 | const data = await readXLSX(file); 50 | 51 | users = [...users, data]; 52 | } 53 | 54 | return users.reduce((curr, acc) => curr.concat(acc)); 55 | } catch (e) { 56 | console.log(e); 57 | process.exit(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /handler/message.js: -------------------------------------------------------------------------------- 1 | import emitHandler from "../routes/message.js"; 2 | import { botInfo } from "../common/message.js"; 3 | 4 | const handler = emitHandler(); 5 | 6 | const messageHandler = (client) => async (message) => { 7 | const { body, from, sender, id } = message; 8 | const userNumber = `+${sender.id.replace("@c.us", "")}`; 9 | 10 | const command = body 11 | .slice(process.env.PREFIX.length) 12 | .trim() 13 | .split(/ +/) 14 | .shift() 15 | .toLowerCase(); 16 | const args = body 17 | .slice(process.env.PREFIX.length) 18 | .trim() 19 | .split(/ +/) 20 | .slice(1); 21 | 22 | switch (command) { 23 | case "a": 24 | case "absen": 25 | return await handler.emit("absen", { 26 | args, 27 | client, 28 | message, 29 | userNumber, 30 | }); 31 | case "buat": 32 | case "gen": 33 | case "generate": 34 | return await handler.emit("generate", { 35 | client, 36 | message, 37 | userNumber, 38 | }); 39 | case "list": 40 | case "lists": 41 | return await handler.emit("lists", { 42 | args, 43 | client, 44 | message, 45 | userNumber, 46 | }); 47 | case "created": 48 | case "terbuat": 49 | case "absen-terbuat": 50 | return await handler.emit("list-created", { 51 | client, 52 | message, 53 | userNumber, 54 | }); 55 | case "del": 56 | case "delete": 57 | case "hapus": 58 | return await handler.emit("delete", { 59 | args, 60 | client, 61 | message, 62 | userNumber, 63 | }); 64 | case "perintah": 65 | case "help": 66 | case "command": 67 | case "commands": 68 | return await handler.emit("help", { 69 | client, 70 | message, 71 | }); 72 | case "me": 73 | case "saya": 74 | return await handler.emit("me", { 75 | client, 76 | message, 77 | userNumber, 78 | }); 79 | default: 80 | await client.simulateTyping(message.from, true); 81 | await client.reply( 82 | from, 83 | command.length > 0 84 | ? `Tidak ada perintah yang bernama '${command}'` 85 | : botInfo, 86 | id, 87 | true 88 | ); 89 | await client.simulateTyping(message.from, false); 90 | } 91 | 92 | return true; 93 | }; 94 | 95 | export default messageHandler; 96 | -------------------------------------------------------------------------------- /common/studentSorter.js: -------------------------------------------------------------------------------- 1 | import parsePhoneNumber from "libphonenumber-js"; 2 | import { NameRegex, xlsxSheetSplitter } from "../utils/regex.js"; 3 | import whatsRole from "../utils/whatsRole.js"; 4 | 5 | import Class from "../models/class.js"; 6 | 7 | const { isPossiblePhoneNumber, isValidPhoneNumber } = parsePhoneNumber; 8 | 9 | const studentSorter = async (sheetName, reject, file) => { 10 | const splittedSheetname = sheetName.split(xlsxSheetSplitter); 11 | 12 | const gradeClass = await Class.findOne({ 13 | gradeName: splittedSheetname[0], 14 | }).lean(); 15 | 16 | if (!gradeClass) 17 | return reject( 18 | `Kelas ${sheetName} tidak pernah ada ! File: ${sheetName}.xlsx` 19 | ); 20 | 21 | const className = gradeClass.classNames.find( 22 | (className) => className.name === splittedSheetname[1] 23 | ); 24 | 25 | if (!className) 26 | return reject( 27 | `Tidak ada kelas yang bernama '${splittedSheetname[1]}' di kelas ${gradeClass.gradeName} ! File: ${sheetName}.xlsx` 28 | ); 29 | 30 | return (d) => { 31 | const keys = Object.keys(d); 32 | 33 | if ( 34 | !keys.includes("NAMA SISWA") | 35 | !keys.includes("NO TELEPON") | 36 | !keys.includes("ROLE") 37 | ) 38 | reject( 39 | `Format tidak sesuai. Judul data harus menggunakan "NAMA SISWA", "NO TELEPON", "ROLE". File: ${file}` 40 | ); 41 | 42 | const roles = whatsRole(d.ROLE); 43 | if (roles instanceof Error) 44 | reject(`${roles} File: ${file}, Nama Siswa: ${d["NAMA SISWA"].trim()}`); 45 | 46 | const reformatted = { 47 | name: d["NAMA SISWA"] 48 | .toLowerCase() 49 | .trim() 50 | .replace(/\b(\w)/g, (s) => s.toUpperCase()), 51 | phoneNumber: d["NO TELEPON"].trim().replace(".", "+"), 52 | roles, 53 | className: { 54 | grade: gradeClass._id, 55 | gradeName: className._id, 56 | }, 57 | }; 58 | 59 | if ( 60 | !isPossiblePhoneNumber(reformatted.phoneNumber, "ID") && 61 | !isValidPhoneNumber(reformatted.phoneNumber, "ID") 62 | ) 63 | reject( 64 | `Nomor telepon dari siswa yang bernama '${reformatted.name}' salah. File: ${file}` 65 | ); 66 | 67 | if (!NameRegex.test(reformatted.name)) 68 | reject( 69 | `Siswa bernama ${reformatted.name} namanya salah, coba di cek kembali apakah nama tersebut sudah benar. File: ${file}` 70 | ); 71 | 72 | return reformatted; 73 | }; 74 | }; 75 | 76 | export default studentSorter; 77 | -------------------------------------------------------------------------------- /common/message.js: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | 3 | const packageInfo = JSON.parse( 4 | readFileSync(new URL("../package.json", import.meta.url)) 5 | ); 6 | 7 | const GITHUB_URL = packageInfo.repository.url 8 | .replace("git+", "") 9 | .replace(".git", ""); 10 | 11 | // Daftar perintah kalau help dipanggil 12 | const helpReply = `Nesbaen, saya adalah bot absen. 13 | Prefix: ${process.env.PREFIX} 14 | 15 | Bot ini bisa digunakan di chat pribadi atau di grup. 16 | 17 | Catatan: Anda harus terdaftar terlebih dahulu. Hubungi host dari bot ini untuk didaftarkan. 18 | 19 | Daftar Perintah: 20 | - saya: Perintah ini digunakan untuk mengecek profil diri sendiri. 21 | 22 | Contoh: ${process.env.PREFIX} saya 23 | 24 | - absen: Perintah ini adalah untuk absen, diperlukan argumen "id" yang unik. Anda harus memiliki role siswa. 25 | 26 | Contoh: ${process.env.PREFIX} absen 27 | 28 | - buat: Perintah ini digunakan untuk membuat absen, gunakan secara bijak. Anda harus memiliki role pengurus/guru/admin. 29 | 30 | Jika sudah diberikan id uniknya, berikan ke orang/kelas yang dituju. 31 | 32 | Contoh: ${process.env.PREFIX} buat 33 | 34 | - list: Perintah ini digunakan untuk menampilkan siapa saja yang sudah absen, diperlukan argumen "id" yang unik. Anda harus memiliki role pengurus/guru/admin. 35 | 36 | Contoh: ${process.env.PREFIX} list 37 | 38 | - terbuat: Perintah ini digunakan untuk mendapatkan list daftar absen yang pernah dibuat oleh anda. Diperlukan role pengurus/guru/admin. List absensi bisa didapatkan oleh orang yang membuat beberapa absen sebelumnya. 39 | 40 | Misal 41 | "A" telah membuat 3 absen, jika "A" memanggil perintah ini maka dia bisa mendapatkan daftar 3 absen yang ia pernah buat sebelumnya. 42 | 43 | Contoh: ${process.env.PREFIX} terbuat 44 | 45 | - hapus: Perintah ini digunakan untuk *menghapus* absen, diperlukan argumen "id" yang unik. Anda harus memiliki role pengurus/guru/admin. Yang bisa menghapus absen adalah orang yang membuatnya. 46 | 47 | Catatan: *Hati-hati* dalam menggunakan perintah ini. Sekali terhapus sudah terhapus untuk selamanya. 48 | 49 | Contoh: ${process.env.PREFIX} hapus 50 | 51 | 52 | Sumber Kode: ${GITHUB_URL} 53 | 54 | Dibuat oleh Ezra Khairan Permana di bawah lisensi MIT.`; 55 | 56 | // Informasi Bot 57 | const botInfo = `Nesbaen, saya adalah bot absen. 58 | 59 | Untuk perintah lengkap ketik: 60 | "${process.env.PREFIX} help" (tanpa tanda "). 61 | 62 | Sumber Kode: ${GITHUB_URL} 63 | 64 | Dibuat oleh Ezra Khairan Permana di bawah lisensi MIT.`; 65 | 66 | export { GITHUB_URL, helpReply, botInfo }; 67 | -------------------------------------------------------------------------------- /controller/message.js: -------------------------------------------------------------------------------- 1 | import Absen from "../models/absen.js"; 2 | import Class from "../models/class.js"; 3 | 4 | import { isUUID4 } from "../validator/argumentsValidator.js"; 5 | import { generateDateString } from "../utils/date.js"; 6 | import { checkUser } from "../utils/permittedOrNot.js"; 7 | 8 | export default { 9 | absen: async ({ args, client, message, user }) => { 10 | await client.simulateTyping(message.from, true); 11 | const valid = await isUUID4({ args, client, message }); 12 | 13 | if (valid) { 14 | const absensi = await Absen.findOne({ absen_id: args[0] }); 15 | 16 | if (!absensi) { 17 | await client.simulateTyping(message.from, false); 18 | return await client.reply( 19 | message.from, 20 | "Absensi tidak ada !", 21 | message.id, 22 | true 23 | ); 24 | } 25 | 26 | const alreadyPresence = absensi.userList.some(({ user_id }) => 27 | user_id.equals(user._id) 28 | ); 29 | 30 | if (alreadyPresence) { 31 | await client.simulateTyping(message.from, false); 32 | return await client.reply( 33 | message.from, 34 | "Anda sudah absen sebelumnya.", 35 | message.id, 36 | true 37 | ); 38 | } 39 | absensi.userList.push({ 40 | user_id: user._id, 41 | precense_time: new Date(Number(`${message.t}000`)), 42 | }); 43 | 44 | await absensi.save(); 45 | 46 | await client.reply( 47 | message.from, 48 | "Berhasil terabsen 👍", 49 | message.id, 50 | true 51 | ); 52 | return await client.simulateTyping(message.from, false); 53 | } 54 | }, 55 | 56 | generate: async ({ client, message, user }) => { 57 | await client.simulateTyping(message.from, true); 58 | const absensi = new Absen({ 59 | assignator: user._id, 60 | created_at: new Date(Number(`${message.t}000`)), 61 | }); 62 | 63 | await absensi.save(); 64 | 65 | await client.reply( 66 | message.from, 67 | "Salin perintah absen di bawah ini, kirimkan ke teman anda.", 68 | message.id, 69 | true 70 | ); 71 | 72 | await client.sendText( 73 | message.from, 74 | `${process.env.PREFIX} absen ${absensi.absen_id}` 75 | ); 76 | return await client.simulateTyping(message.from, false); 77 | }, 78 | 79 | listCreated: async ({ client, message, user }) => { 80 | await client.simulateTyping(message.from, true); 81 | 82 | const absensi = await Absen.find({ assignator: user._id }).lean(); 83 | 84 | if (absensi.length === 0) { 85 | await client.simulateTyping(message.from, false); 86 | return await client.reply( 87 | message.from, 88 | "Anda belum pernah membuat daftar absensi.", 89 | message.id, 90 | true 91 | ); 92 | } 93 | 94 | await client.reply( 95 | message.from, 96 | `Daftar absen yang dibuat oleh anda, ${user.name}`, 97 | message.id, 98 | true 99 | ); 100 | 101 | for (const absen of absensi) { 102 | await client.sendText( 103 | message.from, 104 | `${absen.absen_id} | ${generateDateString(absen.created_at)}` 105 | ); 106 | } 107 | 108 | return await client.simulateTyping(message.from, false); 109 | }, 110 | 111 | lists: async ({ args, client, message }) => { 112 | await client.simulateTyping(message.from, true); 113 | const valid = await isUUID4({ args, client, message }); 114 | 115 | if (valid) { 116 | const absensi = await Absen.findOne({ absen_id: args[0] }) 117 | .populate("userList.user_id") 118 | .populate("assignator") 119 | .lean(); 120 | 121 | if (!absensi) { 122 | await client.simulateTyping(message.from, false); 123 | return await client.reply( 124 | message.from, 125 | "Absensi tidak ada !", 126 | message.id, 127 | true 128 | ); 129 | } 130 | 131 | await client.simulateTyping(message.from, false); 132 | return await client.reply( 133 | message.from, 134 | `LIST ABSENSI\n\nAbsen dibuat oleh: ${ 135 | absensi.assignator.name 136 | }\nTanggal dibuat: ${generateDateString(absensi.created_at)} 137 | 138 | ${ 139 | absensi.userList.length > 0 140 | ? `Daftar Yang Hadir:${absensi.userList.map( 141 | (user, idx) => `\n${idx + 1}. ${user.user_id.name}` 142 | )}` 143 | : "Belum ada yang absen." 144 | }`.trim(), 145 | message.id, 146 | true 147 | ); 148 | } 149 | }, 150 | 151 | delete: async ({ args, client, message, user }) => { 152 | await client.simulateTyping(message.from, true); 153 | const valid = await isUUID4({ args, client, message }); 154 | 155 | if (valid) { 156 | const absensi = await Absen.findOne({ absen_id: args[0] }) 157 | .populate("assignator") 158 | .lean(); 159 | 160 | if (!absensi) { 161 | await client.simulateTyping(message.from, false); 162 | return await client.reply( 163 | message.from, 164 | "Absensi tidak ada !", 165 | message.id, 166 | true 167 | ); 168 | } 169 | 170 | if (!absensi.assignator._id.equals(user._id)) { 171 | await client.simulateTyping(message.from, false); 172 | return await client.reply( 173 | message.from, 174 | "Anda bukan pembuat absen aslinya !", 175 | message.id, 176 | true 177 | ); 178 | } 179 | 180 | await Absen.deleteOne({ 181 | absen_id: absensi.absen_id, 182 | assignator: absensi.assignator._id, 183 | }); 184 | 185 | await client.reply( 186 | message.from, 187 | `Berhasil menghapus absen 👍\nID: ${absensi.absen_id}`, 188 | message.id, 189 | true 190 | ); 191 | return await client.simulateTyping(message.from, false); 192 | } 193 | }, 194 | 195 | me: async ({ client, message, userNumber }) => { 196 | await client.simulateTyping(message.from, true); 197 | const user = await checkUser({ client, message, userNumber }); 198 | 199 | if (user) { 200 | const gradeClassName = user.className 201 | ? await Class.findOne({ _id: user.className.grade }).lean() 202 | : null; 203 | 204 | const className = gradeClassName 205 | ? gradeClassName.classNames.find((classes) => 206 | classes._id.equals(user.className.gradeName) 207 | ) 208 | : null; 209 | 210 | await client.reply( 211 | message.from, 212 | `Profil anda 213 | 214 | Nama: ${user.name} 215 | ${ 216 | gradeClassName 217 | ? `Kelas: ${gradeClassName.gradeName} ${className.name}\n` 218 | : "\n" 219 | }Roles: ${user.roles.join(", ")} 220 | Tanggal didaftarkan: ${generateDateString(user.created_at)}`, 221 | message.id, 222 | true 223 | ); 224 | return await client.simulateTyping(message.from, false); 225 | } 226 | }, 227 | }; 228 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NESBAEN 2 | 3 | Ini adalah aplikasi absen berbasis bot whatsapp, kata dasar nesbaen adalah absen. Aplikasi ini memiliki fitur untuk mengabsen dan membuat absensi baru lewat whatsapp secara otomatis dengan sistem user yang terdaftar. 4 | 5 | ## Latar belakang bot ini dibuat 6 | 7 | 1. Jika absen di whatsapp secara biasa tidaklah efisien, menunggu teman dan menyalin daftar absensi yang sebelumnya dan mengetikan nama, padahal data absensi sudah ada di daftar absen sekolah. 8 | 2. Kasus "gw duluan" dalam absen itu menganggu apalagi terkadang menimbulkan keributan, padahal hal seperti ini adalah hal yang sepele. 9 | 3. Mengisi google form dengan kolom tanggal dan waktu secara berulang-ulang setiap harinya adalah hal yang buang-buang waktu. Terkadang pengaturan moderasinya tidak selalu benar dan menganggu konsentrasi siswa saat belajar. 10 | 11 | ## Prerequisites 12 | 13 | Anda butuh 14 | 15 | - Node.js Dan NPM 16 | - MongoDB untuk menyimpan data 17 | - Handphone tak terpakai (opsional, direkomendasikan) 18 | 19 | ## Pemakaian 20 | 21 | ### Ikhtisar Daftar Perintah 22 | 23 | Berikut daftar perintah yang sudah dibuat. 24 | 25 | | **Nama Perintah** | **Deskripsi Penggunaan** | **Contoh** | 26 | | :---------------: | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-----------: | 27 | | saya | Perintah ini digunakan untuk mengecek profil diri sendiri. | N# saya | 28 | | buat | Perintah ini digunakan untuk membuat absen, gunakan secara bijak.
Anda harus memiliki role pengurus/guru/admin.

Jika sudah diberikan id uniknya, berikan ke orang/kelas yang dituju. | N# buat | 29 | | list | Perintah ini digunakan untuk menampilkan siapa saja yang sudah absen,
diperlukan argumen "id" yang unik.

Anda harus memiliki role pengurus/guru/admin. | N# list | 30 | | terbuat | Perintah ini digunakan untuk mendapatkan list daftar absen yang pernah dibuat oleh anda.
Diperlukan role pengurus/guru/admin. List absensi bisa didapatkan oleh orang
yang membuat beberapa absen sebelumnya.

Misal
"A" telah membuat 3 absen, jika "A" memanggil perintah ini maka dia
bisa mendapatkan daftar 3 absen yang ia pernah buat sebelumnya. | N# terbuat | 31 | | hapus | Perintah ini digunakan untuk _menghapus_ absen, diperlukan argumen "id" yang unik.
Anda harus memiliki role pengurus/guru/admin.
Yang bisa menghapus absen adalah orang yang membuatnya.

Catatan: **Hati-hati** dalam menggunakan perintah ini. Sekali terhapus sudah terhapus untuk selamanya. | N# hapus | 32 | 33 | Atau gunakan `N# help` pada saat bot sudah hidup. 34 | 35 | ### Cloning dari Github 36 | 37 | Jalankan perintah ini Command Line. 38 | 39 | ```sh 40 | # https 41 | git clone https://github.com/reacto11mecha/nesbaen.git 42 | 43 | # SSH 44 | git clone git@github.com:reacto11mecha/nesbaen.git 45 | ``` 46 | 47 | ### Menginstall package 48 | 49 | Anda ke root directory project dan menjalankan 50 | 51 | ```sh 52 | npm install 53 | ``` 54 | 55 | ### Menjalankan Aplikasinya 56 | 57 | Pertama-tama, copy file `env.example` menjadi `.env` dan isikan value yang sesuai. 58 | 59 | > Saran, prefix dari bot nya tidak usah diubah. 60 | 61 | Kedua, copy folder `example` menjadi `sensitive` dan isikan file dengan file excel yang sesuai yang sesuai. 62 | 63 | --- 64 | 65 | Ada aturan dalam penulisan file xlsx, diantaranya. 66 | 67 | 1. File yang digunakan harus berekstensi/berakhiran `.xlsx` 68 | 2. Nama file yang dipakai harus menggunakan huruf kapital 69 | 3. Isikan file tersebut hanya dengan satu sheet 70 | 4. Nama sheet harus sama dengan nama file (tanpa ekstensi `.xlsx`) 71 | 5. Nama guru atau siswa harus ditulis dengan huruf kapital 72 | 6. Penulisan nomor telepon harus dengan format kode telepon +62, cara mudahnya lihat kontak seseorang lewat info whatsapp 73 | 7. Tulisan di excel harus menggunakan `'.62XXXX`. Di hapus tanda `-` dan hilangkan spasi. 74 | 75 | > Jalan mudahnya tinggal ikuti file contoh yang sudah ada 76 | 77 | --- 78 | 79 | Format penamaan file excel. Ada beberapa penamaan yang bisa digunakan. 80 | 81 | 1. `GURU.xlsx`: Untuk daftar nama guru dengan nomor teleponnya 82 | 2. `(TINGKATAN KELAS DALAM HURUF ROMAWI KAPITAL) (NAMA KELAS, misal BAHASA).xlsx`: Ini adalah nama file yang dinamis dengan catatan tingkatan kelas dengan nama kelas harus ada di database, misal `X BAHASA.xlsx`, `X IPA 5.xlsx`, `X IPS 2.xlsx`. 83 | 84 | > Jalan mudahnya tinggal ikuti file contoh yang sudah ada 85 | 86 | --- 87 | 88 | Untuk menjalankan botnya, pastikan mongodb sudah hidup dan url-nya benar. 89 | 90 | ```sh 91 | npm start 92 | ``` 93 | 94 | Jika baru pertama kali menjalankan, scan barcode di terminal untuk dihubungkan ke whatsapp di handphone. 95 | 96 | ### Contoh Pemakaian 97 | 98 | Ini adalah contoh pemakaian dari nesbaen, dibawah ini screenshot pemakaiannya. Pertama, pemanggilan bot menggunakan `N#` biasa, akan menampilkan perintah yang kurang lebih sama seperti `N# help`. 99 | 100 | ![Memanggil bot dengan prefix yang valid](./assets/SS1.png) 101 | 102 | Mengecek profil diri sendiri menggunakan `N# saya`. Jika tidak terdaftar maka tidak akan menampilkan profil dan akan mengirimkan bahwa yang mengirimkan pesan tidak terdaftar. 103 | 104 | ![Mengecek profil apakah sudah terdaftar atau belum](./assets/SS2.png) 105 | 106 | Jika memiliki role pengurus/guru/admin maka bisa menggunakan perintah `buat` untuk membuat absen, nanti akan dikirimkan pesan berupa absen dan id dari absennya. 107 | 108 | ![Membuat absensi baru](./assets/SS3.png) 109 | 110 | Karena pesan sebelumnya sudah mengirimkan pesan absen beserta id nya. Maka dengan mudah kita tinggal menyalin atau meneruskan pesannya ke bot. 111 | 112 | ![Absensi ke bot](./assets/SS4.png) 113 | 114 | Jika kamu telah membuat absen sebagai role yang sudah disebutkan sebelumnya dengan nomor telepon yang sama, kamu bisa mengecek absen yang sudah-sudah sebelumnya dengan menggunakan `N# terbuat`. 115 | 116 | ![Cek daftar-daftar absensi yang telah dibuat sebelumnya](./assets/SS5.png) 117 | 118 | Jika sebelumnya itu absen menggunakan id yang telah diberikan, orang dengan role yang sudah disebutkan bisa mengecek siapa saja yang sudah absen dengan id tertentu. Gunakan `N# list ` untuk mengecek siapa saja yang sudah absen, contohnya seperti dibawah ini. 119 | 120 | ![Mengecek siapa saja yang sudah absen dengan id tertentu](./assets/SS6.png) 121 | 122 | Menghapus absensi yang sudah dibuat bisa menggunakan `N# hapus `. Yang bisa menghapus adalah orang yang memiliki ketiga role khusus dan orang yang membuat absensi dengan id yang sama. 123 | 124 | ![Menghapus absensi](./assets/SS7.png) 125 | 126 | ### Deploy ke Heroku 127 | 128 | Jika ingin mendeploy ke heroku, klik tombol dibawah ini. 129 | 130 | [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/reacto11mecha/nesbaen/) 131 | --------------------------------------------------------------------------------