diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..4100ef2 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,4 @@ +firebase-key.json +package-lock.json +.vscode/ +node_modules/ \ No newline at end of file diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..c5609aa --- /dev/null +++ b/backend/package.json @@ -0,0 +1,25 @@ +{ + "name": "smart-github", + "version": "1.1.0", + "description": "Chrome extension for github", + "main": "src/index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/ygnoh/smart-github.git" + }, + "keywords": [], + "author": "Yonggoo Noh (https://github.com/ygnoh)", + "license": "AGPL-3.0-only", + "bugs": { + "url": "https://github.com/ygnoh/smart-github/issues" + }, + "homepage": "https://github.com/ygnoh/smart-github#readme", + "dependencies": { + "express": "^4.16.3", + "googleapis": "^29.0.0", + "request": "^2.85.0" + } +} diff --git a/backend/src/index.js b/backend/src/index.js new file mode 100644 index 0000000..cf60d4e --- /dev/null +++ b/backend/src/index.js @@ -0,0 +1,76 @@ +const express = require("express"); +const request = require("request"); +const {google} = require("googleapis"); + +const app = express(); +const PORT = 3000; + +let accessToken; +getAccessToken().then(token => accessToken = token); + +app.post("/watch", (req, res, next) => { + let payload = ""; + + req.on("readable", () => { + const read = req.read(); + if (!read) { + return; + } + + payload += read; + }); + + req.on("end", () => { + try { + const {comment} = JSON.parse(payload); + const {html_url, user, body} = comment; + + console.log(`${user.login}가 당신의 글(${html_url})에 댓글을 달았습니다:\n${body}`); + + request.post({ + url: "https://fcm.googleapis.com/v1/projects/smart-github/messages:send", + json: true, + headers: { + "Content-type": "application/json", + "Authorization": `Bearer ${accessToken}` + }, + body: { + message: { + token: "", // TODO: 타겟 앱 인스턴스 등록 토큰 + notification: { + body: "this is body", + title: "this is title" + } + } + } + }, function(error, incomingMessage, response) { + console.log(error); + }); + } catch (e) { + console.error(e); + } + }); +}); + +app.listen(PORT, () => console.log(`\nStart to listen on port ${PORT}.\n`)); + +function getAccessToken() { + return new Promise(function (resolve, reject) { + // the private key to get an access token + const key = require('../firebase-key.json'); + const jwtClient = new google.auth.JWT( + key.client_email, + null, + key.private_key, + ["https://www.googleapis.com/auth/firebase.messaging"], + null + ); + jwtClient.authorize(function (err, tokens) { + if (err) { + reject(err); + return; + } + resolve(tokens.access_token); + }); + }); +} \ No newline at end of file diff --git a/.babelrc b/frontend/.babelrc similarity index 100% rename from .babelrc rename to frontend/.babelrc diff --git a/.gitignore b/frontend/.gitignore similarity index 100% rename from .gitignore rename to frontend/.gitignore diff --git a/package.json b/frontend/package.json similarity index 92% rename from package.json rename to frontend/package.json index 465034a..f8f6a83 100644 --- a/package.json +++ b/frontend/package.json @@ -4,7 +4,8 @@ "description": "Chrome extension for github", "main": "src/background.js", "dependencies": { - "babel-runtime": "^6.26.0" + "babel-runtime": "^6.26.0", + "firebase": "^4.13.1" }, "devDependencies": { "babel-core": "^6.26.0", @@ -21,6 +22,7 @@ "copy": "cp -r src/manifest.json src/popup.html src/_locales src/icons dist-dev/", "copy-production": "cp -r src/manifest.json src/popup.html src/_locales src/icons dist/", "build": "webpack && npm run copy", + "build-watch": "webpack --watch && npm run copy", "build-production": "webpack --env.production && npm run copy-production", "clean": "rm -r dist-dev/*", "clean-production": "rm -r dist/*", diff --git a/src/_locales/en/messages.json b/frontend/src/_locales/en/messages.json similarity index 100% rename from src/_locales/en/messages.json rename to frontend/src/_locales/en/messages.json diff --git a/src/_locales/ko/messages.json b/frontend/src/_locales/ko/messages.json similarity index 100% rename from src/_locales/ko/messages.json rename to frontend/src/_locales/ko/messages.json diff --git a/src/background.js b/frontend/src/background.js similarity index 100% rename from src/background.js rename to frontend/src/background.js diff --git a/src/consts/index.js b/frontend/src/consts/index.js similarity index 82% rename from src/consts/index.js rename to frontend/src/consts/index.js index eb2551f..e937da4 100644 --- a/src/consts/index.js +++ b/frontend/src/consts/index.js @@ -1,3 +1,7 @@ +const FIREBASE = { + SERVER_ID: "767779176892" +}; + const MESSAGE = { HOSTS_UPDATED: "hosts-updated", ISSUE_TAB_LOADED: "issue-tab-loaded", @@ -8,5 +12,6 @@ const MESSAGE = { }; export { + FIREBASE, MESSAGE }; \ No newline at end of file diff --git a/src/content.css b/frontend/src/content.css similarity index 100% rename from src/content.css rename to frontend/src/content.css diff --git a/src/content.js b/frontend/src/content.js similarity index 100% rename from src/content.js rename to frontend/src/content.js diff --git a/src/icons/logo128.png b/frontend/src/icons/logo128.png similarity index 100% rename from src/icons/logo128.png rename to frontend/src/icons/logo128.png diff --git a/src/icons/logo16.png b/frontend/src/icons/logo16.png similarity index 100% rename from src/icons/logo16.png rename to frontend/src/icons/logo16.png diff --git a/src/icons/logo24.png b/frontend/src/icons/logo24.png similarity index 100% rename from src/icons/logo24.png rename to frontend/src/icons/logo24.png diff --git a/src/icons/logo32.png b/frontend/src/icons/logo32.png similarity index 100% rename from src/icons/logo32.png rename to frontend/src/icons/logo32.png diff --git a/src/icons/logo48.png b/frontend/src/icons/logo48.png similarity index 100% rename from src/icons/logo48.png rename to frontend/src/icons/logo48.png diff --git a/src/icons/logo64.png b/frontend/src/icons/logo64.png similarity index 100% rename from src/icons/logo64.png rename to frontend/src/icons/logo64.png diff --git a/src/manifest.json b/frontend/src/manifest.json similarity index 80% rename from src/manifest.json rename to frontend/src/manifest.json index 9269d0f..eccbe29 100644 --- a/src/manifest.json +++ b/frontend/src/manifest.json @@ -40,11 +40,14 @@ "permissions": [ "tabs", "webNavigation", - "storage" + "storage", + "gcm" ], "default_locale": "en", "author": "yonggoo.noh@gmail.com", - "homepage_url": "https://github.com/ygnoh/smart-github" + "homepage_url": "https://github.com/ygnoh/smart-github", + + "content_security_policy": "script-src 'self' https://www.gstatic.com/ https://*.firebaseio.com https://www.googleapis.com; object-src 'self'" } \ No newline at end of file diff --git a/src/popup.css b/frontend/src/popup.css similarity index 89% rename from src/popup.css rename to frontend/src/popup.css index 4d80984..212ff61 100644 --- a/src/popup.css +++ b/frontend/src/popup.css @@ -58,22 +58,31 @@ a { color: #0645AD; } +/* body container */ +.sg-popup-body-container { + display: none; +} + +.sg-popup-body-container.is-on { + display: block; +} + /* body */ .sg-popup-body { font-size: 14px; padding: 10px; } -.sg-host-list { +.sg-body-list { padding: 10px; margin-left: 10px; } -.sg-host-list li { +.sg-body-list li { margin-bottom: 5px; } -.sg-popup-heading { +.sg-body-header { font-size: 16px; font-weight: 500; } @@ -85,7 +94,7 @@ a { left: 9.375%; } -#sg-host-input { +.sg-footer-input { font-size: 14px; width: 150px; height: 20px; diff --git a/frontend/src/popup.html b/frontend/src/popup.html new file mode 100644 index 0000000..8e49937 --- /dev/null +++ b/frontend/src/popup.html @@ -0,0 +1,56 @@ + + + + + + + +
+
+ + Smart Github + +
+ + + + +
+
+ Your Hosts: +
    +
    + +
    +
    +
    + Registered usernames: +
      +
      + +
      + +
      + + + + \ No newline at end of file diff --git a/frontend/src/popup.js b/frontend/src/popup.js new file mode 100644 index 0000000..4450689 --- /dev/null +++ b/frontend/src/popup.js @@ -0,0 +1,61 @@ +import "./popup.css"; +import {storage, dom} from "./utils"; + +storage.getHosts().then(hosts => { + const hostListContainer = dom.getHostList(); + + hosts.forEach(host => { + const item = document.createElement("li"); + item.innerHTML = host; + hostListContainer.appendChild(item); + }); +}); + +storage.getUsernames().then(usernames => { + const usernameListContainer = dom.getUsernameList(); + + usernames.forEach(username => { + const item = document.createElement("li"); + item.innerHTML = username; + usernameListContainer.appendChild(item); + }); +}); + +let prevTarget = document.querySelector(".sg-popup-host"); +[].forEach.call(dom.getPopupBtns(), btn => { + btn.addEventListener("change", e => { + prevTarget.classList.remove("is-on"); + const target = document.querySelector(e.target.dataset.target); + target.classList.add("is-on"); + prevTarget = target; + }); +}); +dom.getHostSaveBtn().addEventListener("click", saveHost); +dom.getHostResetBtn().addEventListener("click", storage.resetHosts.bind(storage)); +dom.getUsernameSaveBtn().addEventListener("click", saveUsername); +dom.getUsernameResetBtn().addEventListener("click", storage.resetUsernames.bind(storage)); +dom.getFooterForm().addEventListener("submit", e => { + e.preventDefault(); +}); + +function saveHost() { + const input = dom.getFooterInput(); + let newHost = input.value.trim(); + // 입력 받은 host에 www. 값이 있다면 제거 + newHost = newHost.replace(/^www\./, ""); + if (newHost === "") { + return; + } + + storage.setHosts(newHost); +} + +function saveUsername() { + const username = dom.getFooterInput().value.trim(); + + if (username === "") { + return; + } + + storage.setUsername(username); +} \ No newline at end of file diff --git a/src/utils/dom.js b/frontend/src/utils/dom.js similarity index 89% rename from src/utils/dom.js rename to frontend/src/utils/dom.js index 85faf2c..b6f090d 100644 --- a/src/utils/dom.js +++ b/frontend/src/utils/dom.js @@ -17,17 +17,29 @@ export default { getHostList() { return document.querySelector(".sg-host-list"); }, + getUsernameList() { + return document.querySelector(".sg-username-list"); + }, + getPopupBtns() { + return document.querySelectorAll("input[type=radio][name='sg-popup-buttons']"); + }, getHostSaveBtn() { - return document.getElementById("sg-host-save"); + return document.querySelector(".sg-host-save"); + }, + getUsernameSaveBtn() { + return document.querySelector(".sg-username-save"); }, getHostResetBtn() { - return document.getElementById("sg-host-reset"); + return document.querySelector(".sg-host-reset"); + }, + getUsernameResetBtn() { + return document.querySelector(".sg-username-reset"); }, - getHostInput() { - return document.getElementById("sg-host-input"); + getFooterInput() { + return document.querySelector(".is-on .sg-footer-input"); }, - getHostForm() { - return document.getElementById("sg-host-form"); + getFooterForm() { + return document.querySelector(".is-on .sg-footer-form"); }, removeResetTemplateBtns() { const bottomArea = this.getIssueBottomArea(); diff --git a/src/utils/fetcher.js b/frontend/src/utils/fetcher.js similarity index 100% rename from src/utils/fetcher.js rename to frontend/src/utils/fetcher.js diff --git a/frontend/src/utils/firebase.js b/frontend/src/utils/firebase.js new file mode 100644 index 0000000..6da0a8c --- /dev/null +++ b/frontend/src/utils/firebase.js @@ -0,0 +1,19 @@ +import {FIREBASE} from "../consts"; +import * as firebase from "firebase/app"; +import "firebase/database"; + +const config = { + apiKey: "AIzaSyD7maFJ1fc_lGPQev9Jiyse53AgtCybpJg", + authDomain: "smart-github.firebaseapp.com", + databaseURL: "https://smart-github.firebaseio.com", + projectId: "smart-github", + storageBucket: "smart-github.appspot.com", + messagingSenderId: FIREBASE.SERVER_ID +}; +firebase.initializeApp(config); +const database = firebase.database(); + +export { + firebase, + database +}; \ No newline at end of file diff --git a/src/utils/index.js b/frontend/src/utils/index.js similarity index 76% rename from src/utils/index.js rename to frontend/src/utils/index.js index 6df9ed0..5067ebd 100644 --- a/src/utils/index.js +++ b/frontend/src/utils/index.js @@ -2,10 +2,12 @@ import fetcher from "./fetcher"; import storage from "./storage"; import urlManager from "./urlManager"; import dom from "./dom"; +import firebase from "./firebase"; export { fetcher, storage, urlManager, - dom + dom, + firebase }; \ No newline at end of file diff --git a/src/utils/storage.js b/frontend/src/utils/storage.js similarity index 68% rename from src/utils/storage.js rename to frontend/src/utils/storage.js index ffe8de2..c7a81c2 100644 --- a/src/utils/storage.js +++ b/frontend/src/utils/storage.js @@ -1,4 +1,4 @@ -import {MESSAGE} from "../consts"; +import {FIREBASE, MESSAGE} from "../consts"; /** chrome.storage 관련 작업을 처리하는 객체 */ export default { @@ -19,6 +19,17 @@ export default { }); }); }, + /** + * 등록된 유저 이름들을 가져온다. + * @returns {Array} 등록된 usernames + */ + getUsernames: function () { + return new Promise((resolve, reject) => { + this.get("sg-usernames", result => { + resolve(result["sg-usernames"] || []); + }); + }); + }, /** * 새 host를 추가하고, 페이지를 새로고침한다. * @param {string} newHost 새로 추가할 host @@ -39,6 +50,32 @@ export default { }); }); }, + /** + * 새 username을 추가하고, gcm restration id를 매칭시킨다. 그리고 페이지를 새로고침한다. + * @param {string} newUsername 새로 추가할 username + */ + setUsername: async function (newUsername) { + const usernames = await this.getUsernames(); + + if (usernames.includes(newUsername)) { + return; + } + + chrome.gcm.register(FIREBASE.SENDER_IDS, registrationId => { + /** + * TODO: 서버에게 registration id 전달 + * 어떻게 아이디와 registration id를 매칭 시킬 것인가? + * 단순히 매칭해서 저장하면 사칭할 수 있다. + */ + console.log(registrationId); + }); + + usernames.push(newUsername); + + this.set({ "sg-usernames": usernames }, () => { + location.reload(); + }); + }, /** * 저장된 hosts를 모두 reset하고 페이지를 새로고침한다. */ @@ -49,6 +86,14 @@ export default { }); }); }, + /** + * 저장된 usernames를 모두 reset하고 페이지를 새로고침한다. + */ + resetUsernames: function () { + this.remove("sg-usernames", () => { + location.reload(); + }); + }, /** * 현재 저장소의 token을 얻는다. * @returns {string} 현재 저장소의 token diff --git a/src/utils/urlManager.js b/frontend/src/utils/urlManager.js similarity index 95% rename from src/utils/urlManager.js rename to frontend/src/utils/urlManager.js index 35221a9..39d61b7 100644 --- a/src/utils/urlManager.js +++ b/frontend/src/utils/urlManager.js @@ -61,8 +61,10 @@ export default { * @returns {string} 새 토큰을 생성하는 페이지 URL */ getNewTokenPageUrl: function () { + const scopes = ["repo", "notifications"]; + return `${location.protocol}//${location.host}/settings/tokens/new?` + - `scopes=repo&description=SmartGithub(${location.host})`; + `scopes=${scopes.join(",")}&description=SmartGithub(${location.host})`; }, /** * 현재 페이지에 적용된 템플릿의 이름을 알려주는 함수 diff --git a/webpack.config.js b/frontend/webpack.config.js similarity index 100% rename from webpack.config.js rename to frontend/webpack.config.js diff --git a/src/popup.html b/src/popup.html deleted file mode 100644 index 696ac9a..0000000 --- a/src/popup.html +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - -
      -
      - - Smart Github - -
      -
      - Your Hosts: -
        -
        - -
        - - - - \ No newline at end of file diff --git a/src/popup.js b/src/popup.js deleted file mode 100644 index a05013f..0000000 --- a/src/popup.js +++ /dev/null @@ -1,30 +0,0 @@ -import "./popup.css"; -import {storage, dom} from "./utils"; - -storage.getHosts().then(hosts => { - const hostListContainer = dom.getHostList(); - - hosts.forEach(host => { - const item = document.createElement("li"); - item.innerHTML = host; - hostListContainer.appendChild(item); - }); -}); - -dom.getHostSaveBtn().addEventListener("click", saveHost); -dom.getHostResetBtn().addEventListener("click", storage.resetHosts.bind(storage)); -dom.getHostForm().addEventListener("submit", e => { - e.preventDefault(); -}); - -function saveHost() { - const input = dom.getHostInput(); - let newHost = input.value.trim(); - // 입력 받은 host에 www. 값이 있다면 제거 - newHost = newHost.replace(/^www\./, ""); - if (newHost === "") { - return; - } - - storage.setHosts(newHost); -} \ No newline at end of file