১. ভূমিকা
গুগল ওয়ার্কস্পেস অ্যাড-অন হলো কাস্টমাইজড অ্যাপ্লিকেশন যা জিমেইল, ডকস, শীটস এবং স্লাইডসের মতো গুগল ওয়ার্কস্পেস অ্যাপ্লিকেশনগুলোর সাথে সমন্বিত হয়। এগুলো ডেভেলপারদের কাস্টমাইজড ইউজার ইন্টারফেস তৈরি করতে সক্ষম করে, যা সরাসরি গুগল ওয়ার্কস্পেসের সাথে সংযুক্ত থাকে। অ্যাড-অন ব্যবহারকারীদের কম কনটেক্সট সুইচিংয়ের মাধ্যমে আরও দক্ষতার সাথে কাজ করতে সাহায্য করে।
এই কোডল্যাবে আপনি শিখবেন কীভাবে Node.js, Cloud Run এবং Datastore ব্যবহার করে একটি সহজ টাস্ক লিস্ট অ্যাড-অন তৈরি ও ডেপ্লয় করতে হয়।
আপনি যা শিখবেন
- ক্লাউড শেল ব্যবহার করুন
- ক্লাউড রানে স্থাপন করুন
- একটি অ্যাড-অন ডেপ্লয়মেন্ট ডেসক্রিপ্টর তৈরি এবং ডেপ্লয় করুন
- কার্ড ফ্রেমওয়ার্ক ব্যবহার করে অ্যাড-অন UI তৈরি করুন
- ব্যবহারকারীর প্রতিক্রিয়ায় সাড়া দিন
- একটি অ্যাড-অনে ব্যবহারকারীর প্রেক্ষাপট কাজে লাগান
২. সেটআপ এবং প্রয়োজনীয়তা
একটি গুগল ক্লাউড প্রজেক্ট তৈরি করতে এবং অ্যাড-অনটি যে এপিআই ও সার্ভিসগুলো ব্যবহার করবে সেগুলো সক্রিয় করতে সেটআপ নির্দেশাবলী অনুসরণ করুন।
স্ব-গতিতে পরিবেশ সেটআপ
- ক্লাউড কনসোল খুলুন এবং একটি নতুন প্রজেক্ট তৈরি করুন। (যদি আপনার আগে থেকে Gmail বা Google Workspace অ্যাকাউন্ট না থাকে, তাহলে একটি তৈরি করে নিন ।)
প্রজেক্ট আইডিটি মনে রাখবেন, যা সমস্ত গুগল ক্লাউড প্রজেক্ট জুড়ে একটি অনন্য নাম (উপরের নামটি ইতিমধ্যে ব্যবহৃত হয়েছে এবং আপনার জন্য কাজ করবে না, দুঃখিত!)। এই কোডল্যাবে এটিকে পরবর্তীতে PROJECT_ID হিসাবে উল্লেখ করা হবে।
- এরপরে, গুগল ক্লাউড রিসোর্স ব্যবহার করার জন্য ক্লাউড কনসোলে বিলিং চালু করুন ।
এই কোডল্যাবটি চালাতে খুব বেশি খরচ হওয়ার কথা নয়, এমনকি আদৌ কোনো খরচ নাও হতে পারে। কোডল্যাবের শেষে থাকা "ক্লিন আপ" বিভাগে দেওয়া নির্দেশাবলী অবশ্যই অনুসরণ করবেন, যেখানে রিসোর্সগুলো কীভাবে বন্ধ করতে হবে সে সম্পর্কে পরামর্শ দেওয়া হয়েছে, যাতে এই টিউটোরিয়ালের বাইরে আপনার কোনো বিল না আসে। গুগল ক্লাউডের নতুন ব্যবহারকারীরা ৩০০ মার্কিন ডলারের ফ্রি ট্রায়াল প্রোগ্রামের জন্য যোগ্য।
গুগল ক্লাউড শেল
যদিও গুগল ক্লাউড আপনার ল্যাপটপ থেকে দূরবর্তীভাবে পরিচালনা করা যায়, এই কোডল্যাবে আমরা গুগল ক্লাউড শেল ব্যবহার করব, যা ক্লাউডে চালিত একটি কমান্ড লাইন পরিবেশ।
ক্লাউড শেল সক্রিয় করুন
- ক্লাউড কনসোল থেকে, অ্যাক্টিভেট ক্লাউড শেল-এ ক্লিক করুন।
.
আপনি যখন প্রথমবার ক্লাউড শেল খুলবেন, তখন আপনাকে একটি বর্ণনামূলক স্বাগত বার্তা দেখানো হবে। আপনি যদি স্বাগত বার্তাটি দেখতে পান, তাহলে 'Continue'-তে ক্লিক করুন। স্বাগত বার্তাটি আর প্রদর্শিত হবে না। স্বাগত বার্তাটি নিচে দেওয়া হলো:
ক্লাউড শেল প্রস্তুত করতে এবং এতে সংযোগ স্থাপন করতে মাত্র কয়েক মুহূর্ত সময় লাগবে। সংযোগ করার পর, আপনি ক্লাউড শেল টার্মিনালটি দেখতে পাবেন:
এই ভার্চুয়াল মেশিনটিতে আপনার প্রয়োজনীয় সমস্ত ডেভেলপমেন্ট টুলস লোড করা আছে। এটি একটি স্থায়ী ৫ জিবি হোম ডিরেক্টরি প্রদান করে এবং গুগল ক্লাউডে চলে, যা নেটওয়ার্ক পারফরম্যান্স ও অথেনটিকেশনকে ব্যাপকভাবে উন্নত করে। এই কোডল্যাবে আপনার সমস্ত কাজ একটি ব্রাউজার বা আপনার ক্রোমবুক দিয়ে করা যাবে।
ক্লাউড শেলে সংযুক্ত হওয়ার পর আপনি দেখতে পাবেন যে, আপনাকে ইতিমধ্যেই প্রমাণীকৃত করা হয়েছে এবং প্রজেক্টটি আপনার প্রজেক্ট আইডিতে সেট করা আছে।
- আপনি প্রমাণীকৃত কিনা তা নিশ্চিত করতে ক্লাউড শেলে নিম্নলিখিত কমান্ডটি চালান:
gcloud auth list
যদি আপনাকে GCP API কল করার জন্য Cloud Shell-কে অনুমোদন দিতে বলা হয়, তাহলে Authorize-এ ক্লিক করুন।
কমান্ড আউটপুট
Credentialed Accounts ACTIVE ACCOUNT * <my_account>@<my_domain.com>
সক্রিয় অ্যাকাউন্ট সেট করতে, চালান:
gcloud config set account <ACCOUNT>
আপনি সঠিক প্রজেক্ট নির্বাচন করেছেন কিনা তা নিশ্চিত করতে, ক্লাউড শেলে নিম্নলিখিত কমান্ডটি চালান:
gcloud config list project
কমান্ড আউটপুট
[core] project = <PROJECT_ID>
যদি সঠিক প্রজেক্টটি ফেরত না আসে, তাহলে আপনি এই কমান্ডটি দিয়ে তা সেট করতে পারেন:
gcloud config set project <PROJECT_ID>
কমান্ড আউটপুট
Updated property [core/project].
কোডল্যাবে কমান্ড লাইন অপারেশন এবং ফাইল এডিটিং-এর মিশ্রণ ব্যবহার করা হয়। ফাইল এডিটিং-এর জন্য, আপনি ক্লাউড শেল টুলবারের ডানদিকে থাকা ' ওপেন এডিটর ' বোতামে ক্লিক করে ক্লাউড শেলের বিল্ট-ইন কোড এডিটরটি ব্যবহার করতে পারেন। এছাড়াও ক্লাউড শেলে ভিম (vim) এবং ইম্যাক্স (emacs)-এর মতো জনপ্রিয় এডিটরগুলোও পাওয়া যায়।
৩. ক্লাউড রান, ডেটাস্টোর এবং অ্যাড-অন এপিআই সক্রিয় করুন
ক্লাউড এপিআই সক্ষম করুন
ক্লাউড শেল থেকে, যে কম্পোনেন্টগুলো ব্যবহার করা হবে সেগুলোর জন্য ক্লাউড এপিআইগুলো সক্রিয় করুন:
gcloud services enable \ run.googleapis.com \ cloudbuild.googleapis.com \ cloudresourcemanager.googleapis.com \ datastore.googleapis.com \ gsuiteaddons.googleapis.com
এই প্রক্রিয়াটি সম্পন্ন হতে কয়েক মুহূর্ত সময় লাগতে পারে।
সম্পন্ন হলে, এই ধরনের একটি সফলতার বার্তা প্রদর্শিত হয়:
Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.
একটি ডেটাস্টোর ইনস্ট্যান্স তৈরি করুন
এরপর, অ্যাপ ইঞ্জিন (App Engine) সক্রিয় করুন এবং একটি ডেটাস্টোর (Datastore) ডেটাবেস তৈরি করুন। ডেটাস্টোর ব্যবহার করার জন্য অ্যাপ ইঞ্জিন সক্রিয় করা একটি পূর্বশর্ত, কিন্তু আমরা অন্য কোনো কাজে অ্যাপ ইঞ্জিন ব্যবহার করব না।
gcloud app create --region=us-central gcloud firestore databases create --type=datastore-mode --region=us-central
একটি OAuth সম্মতি স্ক্রিন তৈরি করুন
অ্যাড-অনটি চালাতে এবং ব্যবহারকারীর ডেটার উপর ব্যবস্থা নিতে তার অনুমতির প্রয়োজন। এটি সক্রিয় করতে প্রজেক্টের কনসেন্ট স্ক্রিনটি কনফিগার করুন। কাজ শুরু করার জন্য, কোডল্যাবের ক্ষেত্রে আপনাকে কনসেন্ট স্ক্রিনটিকে একটি ইন্টারনাল অ্যাপ্লিকেশন হিসেবে কনফিগার করতে হবে, যার অর্থ এটি সর্বসাধারণের জন্য বিতরণযোগ্য নয়।
- একটি নতুন ট্যাব বা উইন্ডোতে গুগল ক্লাউড কনসোল খুলুন।
- 'Google Cloud Console'-এর পাশে থাকা নিচের তীরচিহ্নে ক্লিক করুন।
এবং আপনার প্রকল্পটি নির্বাচন করুন। - উপরের বাম কোণায়, মেনুতে ক্লিক করুন।
. - API ও পরিষেবা > ক্রেডেনশিয়াল-এ ক্লিক করুন। আপনার প্রোজেক্টের ক্রেডেনশিয়াল পেজটি প্রদর্শিত হবে।
- OAuth সম্মতি স্ক্রিনে ক্লিক করুন। "OAuth সম্মতি স্ক্রিন" স্ক্রিনটি প্রদর্শিত হবে।
- "ব্যবহারকারীর ধরন"-এর অধীনে, " অভ্যন্তরীণ " নির্বাচন করুন। যদি @gmail.com অ্যাকাউন্ট ব্যবহার করেন, তাহলে "বাহ্যিক " নির্বাচন করুন।
- তৈরি করুন- এ ক্লিক করুন। "অ্যাপ নিবন্ধন সম্পাদনা করুন" নামে একটি পৃষ্ঠা প্রদর্শিত হবে।
- ফর্মটি পূরণ করুন:
- অ্যাপের নামে "Todo Add-on" লিখুন।
- ব্যবহারকারী সহায়তা ইমেল- এ আপনার ব্যক্তিগত ইমেল ঠিকানা লিখুন।
- ডেভেলপার যোগাযোগের তথ্যের অধীনে, আপনার ব্যক্তিগত ইমেল ঠিকানা লিখুন।
- সেভ অ্যান্ড কন্টিনিউ-তে ক্লিক করুন। একটি স্কোপস ফর্ম প্রদর্শিত হবে।
- স্কোপস ফর্ম থেকে, সেভ অ্যান্ড কন্টিনিউ-তে ক্লিক করুন। একটি সারাংশ প্রদর্শিত হবে।
- ড্যাশবোর্ডে ফিরে যেতে ক্লিক করুন।
৪. প্রাথমিক অ্যাড-অনটি তৈরি করুন
প্রকল্পটি শুরু করুন
শুরুতে, আপনি একটি সাধারণ "হ্যালো ওয়ার্ল্ড" অ্যাড-অন তৈরি করে তা ডিপ্লয় করবেন। অ্যাড-অন হলো এক ধরনের ওয়েব সার্ভিস যা https অনুরোধের জবাবে একটি JSON পেলোড পাঠায়, যেখানে ইউজার ইন্টারফেস (UI) এবং করণীয় কাজগুলো বর্ণনা করা থাকে। এই অ্যাড-অনটিতে আপনি Node.js এবং Express ফ্রেমওয়ার্ক ব্যবহার করবেন।
এই টেমপ্লেট প্রজেক্টটি তৈরি করতে, ক্লাউড শেল ব্যবহার করে todo-add-on নামে একটি নতুন ডিরেক্টরি তৈরি করুন এবং সেখানে যান:
mkdir ~/todo-add-on cd ~/todo-add-on
আপনি কোডল্যাবের সমস্ত কাজ এই ডিরেক্টরিতে করবেন।
Node.js প্রজেক্টটি শুরু করুন:
npm init
NPM প্রজেক্ট কনফিগারেশন সম্পর্কে বেশ কিছু প্রশ্ন জিজ্ঞাসা করে, যেমন নাম এবং ভার্সন। প্রতিটি প্রশ্নের জন্য, ডিফল্ট মানগুলো গ্রহণ করতে ENTER চাপুন। ডিফল্ট এন্ট্রি পয়েন্ট হলো index.js নামের একটি ফাইল, যা আমরা পরবর্তীতে তৈরি করব।
এরপর, এক্সপ্রেস ওয়েব ফ্রেমওয়ার্কটি ইনস্টল করুন:
npm install --save express express-async-handler
অ্যাড-অন ব্যাকএন্ড তৈরি করুন
অ্যাপটি তৈরি করা শুরু করার সময় হয়েছে।
index.js নামে একটি ফাইল তৈরি করুন। ফাইল তৈরি করার জন্য, আপনি ক্লাউড শেল উইন্ডোর টুলবারে থাকা ' ওপেন এডিটর' বোতামে ক্লিক করে ক্লাউড শেল এডিটর ব্যবহার করতে পারেন। বিকল্পভাবে, আপনি vim বা emacs ব্যবহার করে ক্লাউড শেলে ফাইল সম্পাদনা ও পরিচালনা করতে পারেন।
index.js ফাইলটি তৈরি করার পর, নিম্নলিখিত বিষয়বস্তু যোগ করুন:
const express = require('express');
const asyncHandler = require('express-async-handler');
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post("/", asyncHandler(async (req, res) => {
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello world!`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
সার্ভারটি 'হ্যালো ওয়ার্ল্ড' বার্তাটি দেখানো ছাড়া আর তেমন কিছু করে না এবং তাতে কোনো সমস্যা নেই। আপনি পরে আরও কার্যকারিতা যোগ করবেন।
ক্লাউড রানে স্থাপন করুন
ক্লাউড রান-এ ডেপ্লয় করার জন্য অ্যাপটিকে কন্টেইনারাইজড করতে হবে।
কন্টেইনার তৈরি করুন
Dockerfile নামে একটি Dockerfile তৈরি করুন যাতে নিম্নলিখিত বিষয়গুলো থাকবে:
FROM node:12-slim
# Create and change to the app directory.
WORKDIR /usr/src/app
# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./
# Install production dependencies.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN npm install --only=production
# Copy local code to the container image.
COPY . ./
# Run the web service on container startup.
CMD [ "node", "index.js" ]
কন্টেইনার থেকে অপ্রয়োজনীয় ফাইলগুলো দূরে রাখুন
কন্টেইনারটিকে হালকা রাখতে, একটি .dockerignore ফাইল তৈরি করুন এবং তাতে নিম্নলিখিত বিষয়গুলো অন্তর্ভুক্ত করুন:
Dockerfile
.dockerignore
node_modules
npm-debug.log
ক্লাউড বিল্ড সক্ষম করুন
এই কোডল্যাবে, নতুন কার্যকারিতা যুক্ত হওয়ার সাথে সাথে আপনি অ্যাড-অনটি একাধিকবার বিল্ড এবং ডিপ্লয় করবেন। কন্টেইনার বিল্ড করা, কন্টেইনার রেজিস্ট্রি-তে পুশ করা এবং ক্লাউড বিল্ড-এ ডিপ্লয় করার জন্য আলাদা আলাদা কমান্ড চালানোর পরিবর্তে, এই প্রক্রিয়াটি পরিচালনা করতে ক্লাউড বিল্ড ব্যবহার করুন। অ্যাপ্লিকেশনটি কীভাবে বিল্ড এবং ডিপ্লয় করতে হবে তার নির্দেশনাসহ একটি cloudbuild.yaml ফাইল তৈরি করুন:
steps:
# Build the container image
- name: 'gcr.io/cloud-builders/docker'
args: ['build', '-t', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME', '.']
# Push the container image to Container Registry
- name: 'gcr.io/cloud-builders/docker'
args: ['push', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME']
# Deploy container image to Cloud Run
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
entrypoint: gcloud
args:
- 'run'
- 'deploy'
- '$_SERVICE_NAME'
- '--image'
- 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
- '--region'
- '$_REGION'
- '--platform'
- 'managed'
images:
- 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
substitutions:
_SERVICE_NAME: todo-add-on
_REGION: us-central1
অ্যাপটি ডেপ্লয় করার জন্য ক্লাউড বিল্ডকে অনুমতি দিতে নিম্নলিখিত কমান্ডগুলো চালান:
PROJECT_ID=$(gcloud config list --format='value(core.project)')
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
--role=roles/run.admin
gcloud iam service-accounts add-iam-policy-binding \
$PROJECT_NUMBER-compute@developer.gserviceaccount.com \
--member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
--role=roles/iam.serviceAccountUser
অ্যাড-অন ব্যাকএন্ড তৈরি এবং স্থাপন করুন
বিল্ড শুরু করতে, ক্লাউড শেলে চালান:
gcloud builds submit
সম্পূর্ণ বিল্ড এবং ডেপ্লয় হতে কয়েক মিনিট সময় লাগতে পারে, বিশেষ করে প্রথমবারের ক্ষেত্রে।
বিল্ড সম্পন্ন হলে, সার্ভিসটি ডেপ্লয় হয়েছে কিনা তা যাচাই করুন এবং URL-টি খুঁজুন। কমান্ডটি চালান:
gcloud run services list --platform managed
এই URL-টি কপি করুন, পরবর্তী ধাপে অ্যাড-অনটি কীভাবে চালু করতে হবে তা গুগল ওয়ার্কস্পেসকে জানানোর জন্য এটি আপনার প্রয়োজন হবে।
অ্যাড-অনটি নিবন্ধন করুন
এখন যেহেতু সার্ভারটি চালু হয়ে গেছে, অ্যাড-অনটির বর্ণনা দিন যাতে গুগল ওয়ার্কস্পেস বুঝতে পারে কীভাবে এটি প্রদর্শন ও চালু করতে হয়।
একটি ডেপ্লয়মেন্ট ডেসক্রিপ্টর তৈরি করুন
নিম্নলিখিত বিষয়বস্তু দিয়ে deployment.json ফাইলটি তৈরি করুন। URL প্লেসহোল্ডারের পরিবর্তে ডেপ্লয় করা অ্যাপের URL ব্যবহার করতে ভুলবেন না।
{
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute"
],
"addOns": {
"common": {
"name": "Todo Codelab",
"logoUrl": "https://raw.githubusercontent.com/webdog/octicons-png/main/black/check.png",
"homepageTrigger": {
"runFunction": "URL"
}
},
"gmail": {},
"drive": {},
"calendar": {},
"docs": {},
"sheets": {},
"slides": {}
}
}
কমান্ডটি চালিয়ে ডিপ্লয়মেন্ট ডেসক্রিপ্টরটি আপলোড করুন:
gcloud workspace-add-ons deployments create todo-add-on --deployment-file=deployment.json
অ্যাড-অন ব্যাকএন্ডে অ্যাক্সেসের অনুমোদন দিন
অ্যাড-অন ফ্রেমওয়ার্কটিরও সার্ভিসটি কল করার জন্য অনুমতির প্রয়োজন। গুগল ওয়ার্কস্পেসকে অ্যাড-অনটি চালু করার অনুমতি দিতে ক্লাউড রানের জন্য IAM পলিসি আপডেট করতে নিম্নলিখিত কমান্ডগুলি চালান:
SERVICE_ACCOUNT_EMAIL=$(gcloud workspace-add-ons get-authorization --format="value(serviceAccountEmail)")
gcloud run services add-iam-policy-binding todo-add-on --platform managed --region us-central1 --role roles/run.invoker --member "serviceAccount:$SERVICE_ACCOUNT_EMAIL"
পরীক্ষার জন্য অ্যাড-অনটি ইনস্টল করুন।
আপনার অ্যাকাউন্টের জন্য ডেভেলপমেন্ট মোডে অ্যাড-অনটি ইনস্টল করতে, ক্লাউড শেলে (Cloud Shell) চালান:
gcloud workspace-add-ons deployments install todo-add-on
একটি নতুন ট্যাব বা উইন্ডোতে (Gmail)[https://mail.google.com/] খুলুন। ডানদিকে, টিক চিহ্ন আইকনযুক্ত অ্যাড-অনটি খুঁজুন।

অ্যাড-অনটি খোলার জন্য, চেকমার্ক আইকনটিতে ক্লিক করুন। অ্যাড-অনটি অনুমোদন করার জন্য একটি অনুরোধ প্রদর্শিত হবে।

'Authorize Access'-এ ক্লিক করুন এবং পপ-আপে দেওয়া অনুমোদন প্রক্রিয়ার নির্দেশাবলী অনুসরণ করুন। এটি সম্পন্ন হলে, অ্যাড-অনটি স্বয়ংক্রিয়ভাবে রিলোড হবে এবং 'Hello world!' বার্তাটি প্রদর্শন করবে।
অভিনন্দন! আপনি এখন একটি সহজ অ্যাড-অন ডেপ্লয় এবং ইনস্টল করেছেন। এবার এটিকে একটি টাস্ক লিস্ট অ্যাপ্লিকেশনে পরিণত করার পালা!
৫. ব্যবহারকারীর পরিচয় অ্যাক্সেস করুন
অনেক ব্যবহারকারী সাধারণত তাদের বা তাদের প্রতিষ্ঠানের ব্যক্তিগত তথ্য নিয়ে কাজ করার জন্য অ্যাড-অন ব্যবহার করেন। এই কোডল্যাবে, অ্যাড-অনটি শুধুমাত্র বর্তমান ব্যবহারকারীর কাজগুলো দেখাবে। ব্যবহারকারীর পরিচয় একটি আইডেন্টিটি টোকেনের মাধ্যমে অ্যাড-অনে পাঠানো হয়, যা ডিকোড করা প্রয়োজন।
ডিপ্লয়মেন্ট ডেসক্রিপ্টরে স্কোপ যোগ করুন
ব্যবহারকারীর পরিচয় ডিফল্টভাবে পাঠানো হয় না। এটি ব্যবহারকারীর ডেটা এবং অ্যাড-অনটির এটি অ্যাক্সেস করার জন্য অনুমতির প্রয়োজন। সেই অনুমতি পেতে, deployment.json ফাইলটি আপডেট করুন এবং অ্যাড-অনটির প্রয়োজনীয় স্কোপের তালিকায় openid এবং email OAuth স্কোপ দুটি যোগ করুন। OAuth স্কোপগুলো যোগ করার পর, ব্যবহারকারীরা পরবর্তীবার এটি ব্যবহার করার সময় অ্যাক্সেস মঞ্জুর করার জন্য অনুরোধ করবে।
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute",
"openid",
"email"
],
এরপর, ক্লাউড শেলে, ডিপ্লয়মেন্ট ডেসক্রিপ্টর আপডেট করতে এই কমান্ডটি চালান:
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
অ্যাড-অন সার্ভার আপডেট করুন
যদিও অ্যাড-অনটি ব্যবহারকারীর পরিচয় চাওয়ার জন্য কনফিগার করা হয়েছে, তবুও এর বাস্তবায়ন আপডেট করা প্রয়োজন।
পরিচয় টোকেন পার্স করুন
প্রথমে প্রজেক্টে গুগল অথোরাইজেশন লাইব্রেরিটি যোগ করুন:
npm install --save google-auth-library
এরপর OAuth2Client রিকোয়ার করার জন্য index.js এডিট করুন:
const { OAuth2Client } = require('google-auth-library');
এরপর আইডি টোকেনটি পার্স করার জন্য একটি হেল্পার মেথড যোগ করুন:
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
ব্যবহারকারীর পরিচয় প্রদর্শন করুন
টাস্ক লিস্টের সমস্ত কার্যকারিতা যোগ করার আগে একবার যাচাই করে নেওয়ার জন্য এটি একটি ভালো সময়। 'হ্যালো ওয়ার্ল্ড'-এর পরিবর্তে ব্যবহারকারীর ইমেল ঠিকানা এবং ইউনিক আইডি প্রিন্ট করার জন্য অ্যাপটির রাউট আপডেট করুন।
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello ${user.email} ${user.sub}`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
এই পরিবর্তনগুলোর পর, index.js ফাইলটি দেখতে এইরকম হবে:
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const card = {
sections: [{
widgets: [
{
textParagraph: {
text: `Hello ${user.email} ${user.sub}`
}
},
]
}]
};
const renderAction = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(renderAction);
}));
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
পুনরায় স্থাপন করুন এবং পরীক্ষা করুন
অ্যাডঅনটি পুনর্নির্মাণ এবং পুনরায় স্থাপন করুন। ক্লাউড শেল থেকে, চালান:
gcloud builds submit
সার্ভারটি পুনরায় স্থাপন করা হলে, Gmail খুলুন বা রিলোড করুন এবং অ্যাড-অনটি আবার খুলুন। যেহেতু স্কোপগুলো পরিবর্তিত হয়েছে, অ্যাড-অনটি পুনরায় অনুমোদনের জন্য অনুরোধ করবে। অ্যাড-অনটিকে আবার অনুমোদন করুন, এবং এটি সম্পন্ন হলে অ্যাড-অনটি আপনার ইমেল ঠিকানা এবং ইউজার আইডি প্রদর্শন করবে।
যেহেতু অ্যাড-অনটি এখন ব্যবহারকারীকে চেনে, আপনি টাস্ক লিস্টের কার্যকারিতা যোগ করা শুরু করতে পারেন।
৬. করণীয় তালিকাটি বাস্তবায়ন করুন।
কোডল্যাবের প্রাথমিক ডেটা মডেলটি বেশ সরল: Task এনটিটিগুলোর একটি তালিকা, যার প্রতিটিতে টাস্কের বর্ণনামূলক টেক্সট এবং একটি টাইমস্ট্যাম্পের জন্য প্রপার্টি রয়েছে।
ডেটাস্টোর ইনডেক্স তৈরি করুন
কোডল্যাবের শুরুতেই প্রজেক্টটির জন্য ডেটাস্টোর সক্রিয় করা হয়েছিল। এর জন্য কোনো স্কিমার প্রয়োজন হয় না, তবে কম্পাউন্ড কোয়েরির জন্য সুস্পষ্টভাবে ইনডেক্স তৈরি করতে হয়। ইনডেক্স তৈরি করতে কয়েক মিনিট সময় লাগতে পারে, তাই আপনি প্রথমে সেটিই করবেন।
নিম্নলিখিত বিষয়বস্তু সহ index.yaml নামে একটি ফাইল তৈরি করুন:
indexes:
- kind: Task
ancestor: yes
properties:
- name: created
তারপর ডেটাস্টোর ইনডেক্সগুলো আপডেট করুন:
gcloud datastore indexes create index.yaml
চালিয়ে যাওয়ার জন্য বলা হলে, আপনার কিবোর্ডের ENTER চাপুন। ইনডেক্স তৈরির কাজটি ব্যাকগ্রাউন্ডে সম্পন্ন হয়। এই কাজটি চলাকালীন, "todos" বাস্তবায়নের জন্য অ্যাড-অন কোড আপডেট করা শুরু করুন।
অ্যাড-অন ব্যাকএন্ড আপডেট করুন
প্রজেক্টে ডেটাস্টোর লাইব্রেরিটি ইনস্টল করুন:
npm install --save @google-cloud/datastore
ডেটাস্টোরে পড়া এবং লেখা
datastore লাইব্রেরি ইম্পোর্ট করা এবং ক্লায়েন্ট তৈরি করার মাধ্যমে শুরু হওয়া করণীয় কাজগুলো বাস্তবায়নের জন্য index.js আপডেট করুন:
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
ডেটাস্টোর থেকে টাস্ক রিড এবং রাইট করার জন্য মেথড যোগ করুন:
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
UI রেন্ডারিং বাস্তবায়ন করুন
বেশিরভাগ পরিবর্তনই অ্যাড-অন UI-তে করা হয়েছে। আগে, UI থেকে দেখানো সমস্ত কার্ড স্থির ছিল – উপলব্ধ তথ্যের ওপর ভিত্তি করে সেগুলো পরিবর্তিত হতো না। এখানে, ব্যবহারকারীর বর্তমান কাজের তালিকার ওপর ভিত্তি করে কার্ডটিকে গতিশীলভাবে তৈরি করতে হবে।
কোডল্যাবের UI-তে একটি টেক্সট ইনপুট এবং কাজগুলোর একটি তালিকা রয়েছে, যেগুলোতে কাজগুলো সম্পন্ন হয়েছে বলে চিহ্নিত করার জন্য চেকবক্স আছে। এগুলোর প্রতিটির একটি onChangeAction প্রপার্টিও রয়েছে, যা ব্যবহারকারী কোনো কাজ যোগ বা মুছে ফেললে অ্যাড-অন সার্ভারে একটি কলব্যাক পাঠায়। এই প্রতিটি ক্ষেত্রে, আপডেট করা কাজের তালিকা দিয়ে UI-টিকে পুনরায় রেন্ডার করতে হয়। এটি সামলানোর জন্য, চলুন কার্ড UI তৈরির একটি নতুন মেথড চালু করা যাক।
index.js সম্পাদনা করা চালিয়ে যান এবং নিম্নলিখিত মেথডটি যোগ করুন:
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
// Input for adding a new task
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
// Create text & checkbox for each task
tasks.forEach(task => taskListSection.widgets.push({
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
}));
} else {
// Placeholder for empty task list
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
}
return card;
}
রুটগুলো আপডেট করুন
এখন যেহেতু ডেটাস্টোরে ডেটা পড়া ও লেখার এবং UI তৈরি করার জন্য হেল্পার মেথড রয়েছে, চলুন অ্যাপ রাউটগুলোতে এগুলোকে একসাথে যুক্ত করি। বিদ্যমান রাউটটি প্রতিস্থাপন করুন এবং আরও দুটি যোগ করুন: একটি টাস্ক যোগ করার জন্য এবং অন্যটি সেগুলো মুছে ফেলার জন্য।
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date()
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
এই হলো চূড়ান্ত ও সম্পূর্ণ কার্যকরী index.js ফাইলটি:
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date()
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
// Input for adding a new task
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
// Create text & checkbox for each task
tasks.forEach(task => taskListSection.widgets.push({
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
}));
} else {
// Placeholder for empty task list
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
}
return card;
}
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
পুনরায় স্থাপন করুন এবং পরীক্ষা করুন
অ্যাড-অনটি পুনর্নির্মাণ ও পুনরায় স্থাপন করতে, একটি বিল্ড শুরু করুন। ক্লাউড শেলে, চালান:
gcloud builds submit
জিমেইলে অ্যাড-অনটি রিলোড করুন, তাহলে নতুন ইউজার ইন্টারফেসটি দেখা যাবে। অ্যাড-অনটি ভালোভাবে দেখতে এক মিনিট সময় নিন। ইনপুট বক্সে কিছু টেক্সট লিখে কিবোর্ডের এন্টার (ENTER) বোতাম চেপে কয়েকটি টাস্ক যোগ করুন, তারপর সেগুলো ডিলিট করার জন্য চেকবক্সে ক্লিক করুন।

আপনি চাইলে এই কোডল্যাবের চূড়ান্ত ধাপে সরাসরি চলে যেতে পারেন এবং আপনার প্রজেক্টটি গুছিয়ে নিতে পারেন। অথবা, আপনি যদি অ্যাড-অন সম্পর্কে আরও জানতে চান, তাহলে আরও একটি ধাপ সম্পন্ন করতে পারেন।
৭. (ঐচ্ছিক) প্রাসঙ্গিক তথ্য যোগ করা
অ্যাড-অনগুলির অন্যতম শক্তিশালী বৈশিষ্ট্য হলো কনটেক্সট-অ্যাওয়ারনেস। ব্যবহারকারীর অনুমতি সাপেক্ষে, অ্যাড-অনগুলি গুগল ওয়ার্কস্পেসের বিভিন্ন কনটেক্সট অ্যাক্সেস করতে পারে, যেমন—ব্যবহারকারী যে ইমেলটি দেখছেন, একটি ক্যালেন্ডার ইভেন্ট এবং একটি ডকুমেন্ট। অ্যাড-অনগুলি কন্টেন্ট ইনসার্ট করার মতো কাজও করতে পারে। এই কোডল্যাবে, আপনি ওয়ার্কস্পেস এডিটরগুলির (ডক্স, শীটস এবং স্লাইডস) জন্য কনটেক্সট সাপোর্ট যোগ করবেন, যাতে এডিটরে থাকাকালীন তৈরি করা যেকোনো টাস্কের সাথে বর্তমান ডকুমেন্টটি সংযুক্ত করা যায়। যখন টাস্কটি প্রদর্শিত হবে, তখন সেটিতে ক্লিক করলে ডকুমেন্টটি একটি নতুন ট্যাবে খুলে যাবে, যা ব্যবহারকারীকে তার কাজটি শেষ করার জন্য মূল ডকুমেন্টে ফিরিয়ে আনবে।
অ্যাড-অন ব্যাকএন্ড আপডেট করুন
newTask রুট আপডেট করুন
প্রথমে, যদি ডকুমেন্ট আইডিটি উপলব্ধ থাকে, তবে টাস্কটিতে সেটি অন্তর্ভুক্ত করার জন্য /newTask রুটটি আপডেট করুন:
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
// Get the current document if it is present
const editorInfo = event.docs || event.sheets || event.slides;
let document = null;
if (editorInfo && editorInfo.id) {
document = {
id: editorInfo.id,
}
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date(),
document,
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
নতুন তৈরি করা টাস্কগুলোতে এখন বর্তমান ডকুমেন্ট আইডি অন্তর্ভুক্ত থাকে। তবে, এডিটরের কনটেক্সট ডিফল্টভাবে শেয়ার করা হয় না। অন্যান্য ইউজার ডেটার মতোই, অ্যাড-অনটিকে ডেটা অ্যাক্সেস করার জন্য ব্যবহারকারীকে অবশ্যই অনুমতি দিতে হবে। তথ্যের অতিরিক্ত শেয়ারিং রোধ করতে, প্রতিটি ফাইলের জন্য আলাদাভাবে অনুমতির অনুরোধ করা এবং তা প্রদান করাই শ্রেয়।
UI আপডেট করুন
index.js ফাইলে buildCard আপডেট করে দুটি পরিবর্তন আনুন। প্রথমটি হলো, টাস্কগুলোর রেন্ডারিং আপডেট করা, যাতে ডকুমেন্টের লিঙ্ক থাকলে তা অন্তর্ভুক্ত থাকে। দ্বিতীয়টি হলো, যদি অ্যাড-অনটি কোনো এডিটরে রেন্ডার করা হয় এবং ফাইল অ্যাক্সেসের অনুমতি এখনো দেওয়া না হয়ে থাকে, তাহলে একটি ঐচ্ছিক অথরাইজেশন প্রম্পট প্রদর্শন করা।
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
tasks.forEach(task => {
const widget = {
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
};
// Make item clickable and open attached doc if present
if (task.document) {
widget.decoratedText.bottomLabel = 'Click to open document.';
const id = task.document.id;
const url = `https://drive.google.com/open?id=${id}`
widget.decoratedText.onClick = {
openLink: {
openAs: 'FULL_SIZE',
onClose: 'NOTHING',
url: url,
}
}
}
taskListSection.widgets.push(widget)
});
} else {
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
};
// Display file authorization prompt if the host is an editor
// and no doc ID present
const event = req.body;
const editorInfo = event.docs || event.sheets || event.slides;
const showFileAuth = editorInfo && editorInfo.id === undefined;
if (showFileAuth) {
card.fixedFooter = {
primaryButton: {
text: 'Authorize file access',
onClick: {
action: {
function: `${baseUrl}/authorizeFile`,
}
}
}
}
}
return card;
}
ফাইল অনুমোদন রুটটি বাস্তবায়ন করুন
অথরাইজেশন বাটনটি অ্যাপে একটি নতুন রুট যোগ করে, তাই চলুন এটি ইমপ্লিমেন্ট করা যাক। এই রুটটি একটি নতুন ধারণা নিয়ে আসে, যার নাম হোস্ট অ্যাপ অ্যাকশন। এগুলো হলো অ্যাড-অনটির হোস্ট অ্যাপ্লিকেশনের সাথে ইন্টারঅ্যাক্ট করার জন্য বিশেষ নির্দেশনা। এক্ষেত্রে, বর্তমান এডিটর ফাইলটিতে অ্যাক্সেসের অনুরোধ করার জন্য এটি ব্যবহৃত হয়।
index.js ফাইলে /authorizeFile রাউটটি যোগ করুন:
app.post('/authorizeFile', asyncHandler(async (req, res) => {
const responsePayload = {
renderActions: {
hostAppAction: {
editorAction: {
requestFileScopeForActiveDocument: {}
}
},
}
};
res.json(responsePayload);
}));
এই হলো চূড়ান্ত ও সম্পূর্ণ কার্যকরী index.js ফাইলটি:
const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();
// Create and configure the app
const app = express();
// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());
// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
action: {
navigations: [{
pushCard: card
}]
}
};
res.json(responsePayload);
}));
app.post('/newTask', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const newTask = formInputs.newTask;
if (!newTask || !newTask.stringInputs) {
return {};
}
// Get the current document if it is present
const editorInfo = event.docs || event.sheets || event.slides;
let document = null;
if (editorInfo && editorInfo.id) {
document = {
id: editorInfo.id,
}
}
const task = {
text: newTask.stringInputs.value[0],
created: new Date(),
document,
};
await addTask(user.sub, task);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task added.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/complete', asyncHandler(async (req, res) => {
const event = req.body;
const user = await userInfo(event);
const formInputs = event.commonEventObject.formInputs || {};
const completedTasks = formInputs.completedTasks;
if (!completedTasks || !completedTasks.stringInputs) {
return {};
}
await deleteTasks(user.sub, completedTasks.stringInputs.value);
const tasks = await listTasks(user.sub);
const card = buildCard(req, tasks);
const responsePayload = {
renderActions: {
action: {
navigations: [{
updateCard: card
}],
notification: {
text: 'Task completed.'
},
}
}
};
res.json(responsePayload);
}));
app.post('/authorizeFile', asyncHandler(async (req, res) => {
const responsePayload = {
renderActions: {
hostAppAction: {
editorAction: {
requestFileScopeForActiveDocument: {}
}
},
}
};
res.json(responsePayload);
}));
function buildCard(req, tasks) {
const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
const inputSection = {
widgets: [
{
textInput: {
label: 'Task to add',
name: 'newTask',
value: '',
onChangeAction: {
function: `${baseUrl}/newTask`,
},
}
}
]
};
const taskListSection = {
header: 'Your tasks',
widgets: []
};
if (tasks && tasks.length) {
tasks.forEach(task => {
const widget = {
decoratedText: {
text: task.text,
wrapText: true,
switchControl: {
controlType: 'CHECKBOX',
name: 'completedTasks',
value: task[datastore.KEY].id,
selected: false,
onChangeAction: {
function: `${baseUrl}/complete`,
}
}
}
};
// Make item clickable and open attached doc if present
if (task.document) {
widget.decoratedText.bottomLabel = 'Click to open document.';
const id = task.document.id;
const url = `https://drive.google.com/open?id=${id}`
widget.decoratedText.onClick = {
openLink: {
openAs: 'FULL_SIZE',
onClose: 'NOTHING',
url: url,
}
}
}
taskListSection.widgets.push(widget)
});
} else {
taskListSection.widgets.push({
textParagraph: {
text: 'Your task list is empty.'
}
});
}
const card = {
sections: [
inputSection,
taskListSection,
]
};
// Display file authorization prompt if the host is an editor
// and no doc ID present
const event = req.body;
const editorInfo = event.docs || event.sheets || event.slides;
const showFileAuth = editorInfo && editorInfo.id === undefined;
if (showFileAuth) {
card.fixedFooter = {
primaryButton: {
text: 'Authorize file access',
onClick: {
action: {
function: `${baseUrl}/authorizeFile`,
}
}
}
}
}
return card;
}
async function userInfo(event) {
const idToken = event.authorizationEventObject.userIdToken;
const authClient = new OAuth2Client();
const ticket = await authClient.verifyIdToken({
idToken
});
return ticket.getPayload();
}
async function listTasks(userId) {
const parentKey = datastore.key(['User', userId]);
const query = datastore.createQuery('Task')
.hasAncestor(parentKey)
.order('created')
.limit(20);
const [tasks] = await datastore.runQuery(query);
return tasks;;
}
async function addTask(userId, task) {
const key = datastore.key(['User', userId, 'Task']);
const entity = {
key,
data: task,
};
await datastore.save(entity);
return entity;
}
async function deleteTasks(userId, taskIds) {
const keys = taskIds.map(id => datastore.key(['User', userId,
'Task', datastore.int(id)]));
await datastore.delete(keys);
}
// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
ডিপ্লয়মেন্ট ডেসক্রিপ্টরে স্কোপ যোগ করুন
সার্ভার পুনর্নির্মাণের আগে, অ্যাড-অন ডিপ্লয়মেন্ট ডেসক্রিপ্টরটি আপডেট করে তাতে https://www.googleapis.com/auth/drive.file OAuth স্কোপটি অন্তর্ভুক্ত করুন। OAuth স্কোপের তালিকায় https://www.googleapis.com/auth/drive.file যোগ করতে deployment.json আপডেট করুন:
"oauthScopes": [
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://www.googleapis.com/auth/calendar.addons.execute",
"https://www.googleapis.com/auth/drive.file",
"openid",
"email"
]
এই ক্লাউড শেল কমান্ডটি চালিয়ে নতুন সংস্করণটি আপলোড করুন:
gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json
পুনরায় স্থাপন করুন এবং পরীক্ষা করুন
অবশেষে, সার্ভারটি পুনর্নির্মাণ করুন। ক্লাউড শেল থেকে, চালান:
gcloud builds submit
কাজটি সম্পন্ন হলে, Gmail খোলার পরিবর্তে, আগে থেকে থাকা কোনো Google ডকুমেন্ট খুলুন অথবা doc.new ফাইলটি খুলে একটি নতুন ডকুমেন্ট তৈরি করুন। নতুন ডকুমেন্ট তৈরি করার সময়, অবশ্যই কিছু টেক্সট লিখুন অথবা ফাইলটির একটি নাম দিন।
অ্যাড-অনটি খুলুন। অ্যাড-অনটির নীচে একটি ‘ফাইল অ্যাক্সেস অনুমোদন করুন’ (Authorize File Access) বোতাম প্রদর্শিত হবে। বোতামটিতে ক্লিক করুন, তারপর ফাইলটিতে অ্যাক্সেস অনুমোদন করুন।
অনুমোদন পাওয়ার পর, এডিটরে থাকাকালীন একটি টাস্ক যোগ করুন। টাস্কটিতে একটি লেবেল থাকে যা নির্দেশ করে যে ডকুমেন্টটি সংযুক্ত করা হয়েছে। লিঙ্কে ক্লিক করলে ডকুমেন্টটি একটি নতুন ট্যাবে খুলে যায়। অবশ্যই, আপনার আগে থেকেই খোলা থাকা ডকুমেন্টটি আবার খোলাটা একটু বোকামি। আপনি যদি বর্তমান ডকুমেন্টের লিঙ্কগুলো ফিল্টার করে বাদ দেওয়ার জন্য UI অপ্টিমাইজ করতে চান, তবে তার জন্য অতিরিক্ত কৃতিত্ব পাবেন!
৮. অভিনন্দন
অভিনন্দন! আপনি ক্লাউড রান ব্যবহার করে সফলভাবে একটি গুগল ওয়ার্কস্পেস অ্যাড-অন তৈরি এবং স্থাপন করেছেন। যদিও কোডল্যাবে একটি অ্যাড-অন তৈরির অনেক মূল ধারণা আলোচনা করা হয়েছে, তবুও আরও অনেক কিছু জানার আছে। নিচের রিসোর্সগুলো দেখুন এবং অতিরিক্ত চার্জ এড়াতে আপনার প্রজেক্টটি পরিষ্করণ করতে ভুলবেন না।
পরিষ্কার করা
আপনার অ্যাকাউন্ট থেকে অ্যাড-অনটি আনইনস্টল করতে, ক্লাউড শেলে এই কমান্ডটি চালান:
gcloud workspace-add-ons deployments uninstall todo-add-on
এই টিউটোরিয়ালে ব্যবহৃত রিসোর্সগুলির জন্য আপনার গুগল ক্লাউড প্ল্যাটফর্ম অ্যাকাউন্টে চার্জ হওয়া এড়াতে:
- ক্লাউড কনসোলে, ম্যানেজ রিসোর্সেস পৃষ্ঠায় যান। উপরের বাম কোণায়, মেনুতে ক্লিক করুন।
আইএএম ও অ্যাডমিন > রিসোর্স ব্যবস্থাপনা ।
- প্রজেক্ট তালিকা থেকে আপনার প্রজেক্টটি নির্বাচন করে ডিলিট-এ ক্লিক করুন।
- ডায়ালগ বক্সে প্রজেক্ট আইডি টাইপ করুন এবং তারপর প্রজেক্টটি মুছে ফেলার জন্য 'শাট ডাউন'-এ ক্লিক করুন।
আরও জানুন
- গুগল ওয়ার্কস্পেস অ্যাড-অনগুলির সংক্ষিপ্ত বিবরণ
- মার্কেটপ্লেসে বিদ্যমান অ্যাপ এবং অ্যাড-অনগুলি খুঁজুন