Merge branch 'master' into coral-plugin-permalink

This commit is contained in:
Kiwi
2017-06-27 23:46:55 +07:00
committed by GitHub
7 changed files with 441 additions and 526 deletions
+1 -525
View File
@@ -1,527 +1,3 @@
# Talk Plugins
Plugins for Talk can take various forms, currently we are only supporting server
side plugins.
## Plugin Registration
The parsing order for the plugin registration is as follows:
- `TALK_PLUGINS_JSON` environment variable
- `plugins.json` file
- `plugins.default.json` file
If you need to "disable all plugins", you can simply provide `{}` as the
contents of `process.env.TALK_PLUGINS_JSON` or the `plugins.json`.
The format for this is thus:
```json
{
"server": [
"people"
]
}
```
Where we have a `server` key with an array of plugins that match the folder
name in the `plugins/` folder. For example, the above config would
require a plugin from `plugins/people`, which must provide a `index.js` file
that returns an object that matches the Plugin Specification.
If the package is external (available on NPM) you can specify the string for
the version by using an object instead, for example:
```json
{
"server": [
{"people": "^1.2.0"}
]
}
```
External plugins can be resolved by running:
```bash
./bin/cli plugins reconcile
```
This achieves two things:
1. It will traverse into local plugin folders and install their dependencies.
_Note that if the plugin is already installed and available in the node_modules folder, it will not be
fetched again unless there is a version mismatch._ This will result in the
project `package.json` and `yarn.lock` files to be modified, this is normal as
this ensures that repeated deployments (with the same config) will have the
same config, these changes should not be committed to source control.
2. It will seek out dependencies that are listed in the object notation and try
to install them from npm.
## Plugin Dependencies
You may also include additional external dependencies in your local packages by
specifying a `package.json` at your plugin root which will result in a
`node_modules` folder being generated at the plugin root with your specific
dependencies.
## Deployment Solutions
Plugins can be deployed with a production instance of Talk.
### Source
Source deployments can just modify the `plugins.json` file and include any
local plugins into the `plugins/` directory. After including the config, you
need to reconcile the plugins and build the static assets:
```bash
# get plugin dependancies and remote plugins
./bin/cli plugins reconcile
# build staic assets (including enabled client side plugins)
yarn build
```
Then the application can be started as is.
### Docker
If you deploy using Docker, you can extend from the `*-onbuild` image, an
example `Dockerfile` for your project could be:
```Dockerfile
FROM coralproject/talk:latest-onbuild
```
Where the directory for your instance would contain a `plugins.json` file
describing the plugin requirements and a `plugins` directory containing any
other local plugins that should be included.
Onbuild triggers will execute when the image is building with your custom
configuration and will ensure that the image is ready to use by building all
assets inside the image as well.
## Server Plugins
### API
You can access any API available inside the talk directory in a plugin by simply
importing the file relative to the talk project root. An example would be if you
wanted to import the `MetadataService`, you would simply write:
```javascript
const MetadataService = require('services/metadata');
```
### Specification
Each plugin should export a single object with all hooks available on it.
_**Note: You will have access to the whole core and other plugin's typeDefs,
context, loaders, mutators, resolvers, hooks. This is intentional, as it
encourages composing plugins to merge functionality, like a Slack plugin which
provides a Slack notify context function as well as having the loader for
comments.**_
The following are the hooks available:
#### Field: `typeDefs`
```graphql
enum COLOUR {
RED
BLUE
}
type Person {
name: String!
colour: COLOUR!
}
type RootMutation {
createPerson(name: String!): Person
}
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
```
Thanks to [gql-merge](https://www.npmjs.com/package/gql-merge) the contents of
`typeDefs` should be a string that will be _merged_ with the existing type
definitions. `enum`'s will be appended to, types will be appended, and new types
will be added.
#### Field: `context`
```js
{
Slack: (context) => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
}
```
Any property provided here will be added to the context parameter available
inside all resolvers, loaders, mutators, and of course, other context based
plugins.
The top level item must accept a context for the request which it should use to
configure the context plugin before it would be mounted at `context.plugins`.
This plugin above would mount at: `context.plugins.Slack`, or, if you're using
[object destructuring](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment), `{plugins: {Slack}}`.
#### Field: `loaders`
```js
(context) => ({
People: {
load: () => db.people.find({user: context.user})
}
})
```
Loaders should be provided as a function which returns a map which is used in
the resolvers function. These must return a promise or a value.
#### Field: `mutators`
```js
(context) => ({
People: {
create: (name) => {
return db.people.insert({user: context.user, name});
}
}
})
```
Mutators should be provided as a function which returns a map which is used in
the resolvers function. These must return a promise or a value.
#### Field: `resolvers`
```js
{
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
}
}
}
```
Should return a resolver map as described in the
[Apollo Docs](http://dev.apollodata.com/tools/graphql-tools/resolvers.html#Resolver-map).
This will merge with the existing resolvers in core and from previous plugins.
#### Field: `hooks`
```js
{
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
}
```
Hooks here are pretty special, for each resolver field, you can specify a
pre/post hook that will execute pre and post field resolution.
If your post function accepts four parameters, then it can modify the field
result. It is *required* that the function resolves a promise (or returns) with
the modified value or simply the original if you didn't modify it.
#### Field: `setupFunctions`
```js
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
},
}),
}
```
Setup functions allow you to create filters that control which pubsub.publish() events
send data to the client. If the type in question contains args, clients may subscribe using those arguments to further filter their subscription.
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
#### Field: `tokenUserNotFound`
```js
tokenUserNotFound: async ({jwt, token}) => {
let profile = await someExternalService(token);
if (!profile) {
return null;
}
let user = await UserModel.findOneAndUpdate({
id: profile.id
}, {
id: profile.id,
username: profile.username,
lowercaseUsername: profile.username.toLowerCase(),
roles: [],
profiles: []
}, {
setDefaultsOnInsert: true,
new: true,
upsert: true
});
return user;
}
```
The `tokenUserNotFound` hook allows auth integrations to hook into the event
when a valid token is provided but a user can't be found in the database that
matches the provided id.
The function is async, and should return the user object that was created in the
database, or null if the user wasn't found. The `jwt` paramenter of the object
is the unpacked token, while `token` is the original jwt token string.
#### Field: `router`
```js
(router) => {
router.get('/api/v1/people', (req, res) => {
res.json({people: [{name: 'Bob'}]});
});
}
```
The Router hook allows you to create a function that accepts the base express
router where you can mount any amount of middleware/routes to do any form of
action needed by external applications. We also provide the authorization
middleware via:
```js
const authorization = require('middleware/authorization');
module.exports = {
router(router) {
router.get('/api/v1/people', authorization.needed('ADMIN'), (req, res) => {
res.json({people: [{name: 'SECRET PEOPLE'}]});
});
}
}
```
#### Field: `tags`
The tags hook allows a plugin to define tags that are code controlled (added
or enabled by code). Below is an example pulled from the core off topic plugin
on how to create a hook for the `OFF_TOPIC` name:
```js
[
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
```
You can refer to `models/schema/tag.js` for the available schema to match when
creating models to enable/disable specific features.
#### Field: `passport`
```js
const FacebookStrategy = require('passport-facebook').Strategy;
const UsersService = require('services/users');
const {ValidateUserLogin, HandleAuthPopupCallback} = require('services/passport');
module.exports = {
passport(passport) {
passport.use(new FacebookStrategy({
clientID: process.env.TALK_FACEBOOK_APP_ID,
clientSecret: process.env.TALK_FACEBOOK_APP_SECRET,
callbackURL: `${process.env.TALK_ROOT_URL}/api/v1/auth/facebook/callback`,
passReqToCallback: true,
profileFields: ['id', 'displayName', 'picture.type(large)']
}, async (req, accessToken, refreshToken, profile, done) => {
let user;
try {
user = await UsersService.findOrCreateExternalUser(profile);
} catch (err) {
return done(err);
}
return ValidateUserLogin(profile, user, done);
}));
},
router(router) {
// Note that we have to import the passport instance here, it is
// instantiated after all the strategies have been mounted.
const {passport} = require('services/passport');
/**
* Facebook auth endpoint, this will redirect the user immediatly to facebook
* for authorization.
*/
router.get('/facebook', passport.authenticate('facebook', {display: 'popup', authType: 'rerequest', scope: ['public_profile']}));
/**
* Facebook callback endpoint, this will send the user a html page designed to
* send back the user credentials upon sucesfull login.
*/
router.get('/facebook/callback', (req, res, next) => {
// Perform the facebook login flow and pass the data back through the opener.
passport.authenticate('facebook', HandleAuthPopupCallback(req, res, next))(req, res, next);
});
}
};
```
This is a full example including the routes hook to add the required components
to the application router to support a different auth strategy.
### Full Example
Contents of `plugins.json`:
```json
{
"server": [
"people"
]
}
```
Located in `plugins/people/index.js`:
```js
module.exports = {
typeDefs: `
enum COLOUR {
RED
BLUE
}
type Person {
name: String!
colour: COLOUR!
}
type RootMutation {
createPerson(name: String!): Person
}
type RootQuery {
people: [Person!]
}
type Subscription {
leader: Person
}
`,
context: {
Slack: () => ({
notify: (message) => {
// return a promise after we're done sending notifications.
}
})
},
loaders: ({user}) => ({
People: {
load: () => db.people.find({user})
}
}),
mutators: ({user}) => ({
People: {
create: (name) => {
return db.people.insert({user, name});
}
}
}),
resolvers: {
Person: {
name(obj, args, context) {
return obj.name;
},
colour(obj, args, context) {
// Bill likes the colour red, everyone else likes blue.
return obj.name === 'bill' ? 'RED' : 'BLUE';
}
},
RootQuery: {
people(obj, args, {loaders: {People}}) {
return People.load();
}
},
RootMutation: {
createPerson(obj, {name}, {mutators: {People}}) {
return People.create(name);
}
}
},
hooks: {
RootMutation: {
createPerson: {
post: async (obj, args, {plugins: {Slack}}, info, person) => {
if (!person) {
return person;
}
await Slack.notify(`A new person just was created with name ${person.name}`);
return person;
}
}
}
},
setupFunctions: {
leader: (options, args) => ({
leader: {
filter: (person) => person.place === 1
}
}
}
};
```
Our documentation for Talk has moved! Utilize [this hyperlink](https://coralproject.github.io/talk/plugins.html) via click or tap to navigate your browser to the new location!
+9
View File
@@ -18,6 +18,9 @@ entries:
- title: Installation
output: web
folderitems:
- title: Getting Started
output: web
url: /install.html
- title: Configuration
output: web
url: /configuration.html
@@ -34,6 +37,12 @@ entries:
- title: Plugins
output: web
folderitems:
- title: Overview
url: /plugins.html
output: web
- title: Quickstart
url: /plugins-quickstart.html
output: web
- title: Client API
url: /plugins-client.html
output: web
+1 -1
View File
@@ -52,7 +52,7 @@ We strive for clear inline documentation across our codebase, but have gaps. Con
If you are considering changing a core component (aka, one that is not in a plugin), you are entering the realm of a core developer. We strongly ask that you reach out the coral team before forking and changing core code. We are glad to help talk through your product need and come up with a strategy for implementing as a plugin, or working with you to extend the plugin API for your use case.
### How do I contribue to these docs?
### How do I contribute to these docs?
Contributions to the docs are much appreciated and a great way to get involved in the project.
+22
View File
@@ -0,0 +1,22 @@
---
title: Installation
sidebar: talk_sidebar
permalink: install.html
summary:
---
## Requirements
Talk requires MongoDB and Redis.
- MongoDB ^3.2 - [Docs](https://docs.mongodb.com/manual/installation/)
- Redis ^3.2.5 - [Docs](https://redis.io/topics/quickstart)
## Installation method
While Talk can be installed in many ways, we support two install paths:
* [Install from Source](install-source.html) (development)
* [Install via Docker](install-docker.html) (deployment)
If you have success installing Talk in another way, please consider [contributing to this documentation](faq.html#how-do-i-contribute-to-these-docs)!
+244
View File
@@ -0,0 +1,244 @@
---
title: Plugins Quickstart
keywords: plugins
sidebar: talk_sidebar
permalink: plugins-quickstart.html
summary:
---
This tutorial walks through the mechanics of creating and publishing a Talk plugin. Along the way I call out some particular habits and techniques that I employ. If you have other practices that you find valuable, please don't hesitate to [contribute!](faq.html#how-do-i-contribute-to-these-docs)
We will create a plugin that exposes a route that allows assets to be created or updated.
## Setup the environment
Before I begin working on the plugin, I've installed [Talk from source](/install-source.html).
### Watch the Server
In a terminal, I run `yarn dev-start`. This:
* starts my server, showing plugin and configuration information
* restarts it when I save files
* shows my temporary `console.log()` statements
* shows real time access logs
* shows verbose debug output if enabled (more on this later)
### Watch the Client build process
In a separate terminal I run `yarn build-watch`. This:
* builds the client side javascript bundles
* watches relevant files and rebuilds the bundle on change
* displays _compile time_ errors, including (the many) syntax errors I cause
If you need to run `yarn install`, you will see missing module error messages here.
### Watch from the Browser
I open up `http://localhost:3000` in a web browser and see the default comment stream. I then open the dev tools console, which:
* shows any _run time_ errors/warnings generated on the front end.
* shows any temporary `console.log()` statements I add during development.
I also often toggle to the Network Tab to see:
* which files are being loaded
* requests sent from my front end code, including headers, the payload/queries sent and the data returned
I strongly recommend taking the time to fully explore all the features of your browser's dev tools!
## Create a home for my new plugin
My goals for this tutorial are to:
* build this plugin locally
* use source control and publish for collaboration
* publish the plugin as an npm library
### Create a repo
I create a new repo called `talk-plugin-asset-manager`. (I use github, but this you could store this anywhere, bitbucket, svn, etc...)
_make sure to respect the naming convention `talk-plugin-*`. This will allow for easy identification of the repo and, eventually, easy searching on npm._
### Set up a local file structure
In a blatant rip off from the Golang community, I create an environment var to hold the path to the root of my Coral directory. This allows absolute pathing.
```
export CORALPATH=/path/to/my/coral/root/dir
```
I like to put my plugins in a directory next to talk, but you could put this anywhere.
```
cd $CORALPATH
git clone https://github.com/jde/talk-plugin-asset-manager.git
```
### Register your plugin
Add the plugin to the plugins.json file:
```
{
"server": [
...
"talk-plugin-asset-manager"
],
"client": [
...
// no client side components so I won't add it here
]
}
```
But wait! Talk looks in `talk/plugins/[plugin-name]` for plugin code. Why couldn't we just add that plugin there?
We could have.
This would make it _a little_ easier to register, but _a lot_ harder to cleanly manage in version control. In order to avoid it being sucked into your Talk repo, you would have to manually `.gitignore` it or use [sub modules]() or something similar.
As a user of a Linux_y_ os, I prefer to create a symbolic link.
```
cd $CORALPATH/talk/plugins
ln -s $CORALPATH/talk-plugin-asset-manager
```
Now, as far as Talk knows, our plugin is right there in the folder. Git is wise, however, and will not include it in the Talk repo. Best of all, our `yarn dev-start` based watch statement follows symbolic links and will restart our sever each time a file is saved.
### Create the initial index file
All plugins contain server and/or client index files, which export all plugin functionality.
```
// talk-plugin-asset-manager/index.js
module.exports = {};
```
## Build the feature!
Now that the plugin is set up I can get down to writing the feature. My goal is to allow my CMS to push new assets as they are created into Talk. To accomplish this, I will create a POST endpoint using Talk's [route api](plugins-server.html#field-router).
### Create a route
When designing my api, I want to be careful to avoid conflicts with not only the existing Talk api, but other plugins in the open source ecosystem that may be creating routes. To do this, I'll follow the golden rule of creating universals with plugins:
_Always namespace all universals with your plugin's unique name._
To ensure everything is hooked up, I'll log the request body (POST payload in this case) to the console and echo it as the response:
```
// talk-plugin-asset-manager/index.js
module.exports = {
router(router) {
router.post('/api/v1/asset-manager', (req, res) => {
console.log(req.body);
res.json(req.body);
});
}
}
```
When I save this file, I reflexively check my console to be sure that the server restarts.
To test that this works, I can:
```
$ curl -H "Content-Type: application/json" -X POST -d '{"url":"http://localhost:3000/my-article.html","title":"My Article"}' http://localhost:3000/api/v1/asset-manager
{"url":"http://localhost:3000/my-article.html","title":"My Article"}
```
After hitting the endpoint, I can also look at the terminal running `yarn dev-start` and see my `console.log()` and the access log:
```
{ url: 'http://localhost:3000/my-article.html',
title: 'My Article' }
POST /api/v1/asset-manager 200 1.379 ms - 68
```
### Save the asset
When I save this asset, I will use Talk's [asset model](https://github.com/coralproject/talk/blob/master/models/asset.js).
Mongo has a handy method [findOneAndUpdate](https://docs.mongodb.com/v3.2/reference/method/db.collection.findOneAndUpdate/) that will take care determining whether or not this asset exists, then either updating or inserting it. Whenever possible, we recommend using these atomic patterns that prevent multiple queries to the db and the efficiency problems and race conditions that they cause.
```
// talk-plugin-asset-manager/index.js
const AssetModel = require('models/asset');
module.exports = {
router(router) {
router.post('/api/v1/asset-manager', (req, res) => {
const asset = req.body;
const update = {$setOnInsert: {url: asset.url}};
AssetModel.findOneAndUpdate(asset, update, {
new: true,
upsert: true,
setDefaultsOnInsert: true
})
.then((asset) => res.json(asset));
});
}
}
```
I can now run the `curl` command as before and see that the response contains a complete asset document!
In addition, I can change the title in the json payload and verify that the id is the same, indicating that the record was updated!
Lastly, I can see the asset in the admin panel at `http://localhost:3000/admin` as well as in my database.
We have an alpha version of our plugin!
### More work to be done
The purpose of this tutorial is to follow the full lifecycle of a plugin, from conception through publication into deployment. With that in mind we'll move forward with this alpha version.
Some things to make this production ready:
* refactoring to separate concerns
* commenting
* adding tests
* validating data
* [adding security](https://github.com/coralproject/talk/blob/b805451d376d2892c81c58d8822a85563e469b88/routes/api/users/index.js#L14)
* incorporating [domain whitelisting](https://github.com/coralproject/talk/blob/b805451d376d2892c81c58d8822a85563e469b88/services/assets.js#L60).
It is important to realize that when you're writing a Talk plugin you are writing a program that may be touched by other devs and could grow in size and complexity. Bring your best engineering sensibilities to bear.
## Publishing the plugin
### Publish to npm
In order to [register](http://localhost:4000/plugins.html#plugin-registration) your _published_ plugin, you will need to [publish it to npm](https://docs.npmjs.com/getting-started/publishing-npm-packages).
Once the package is published, update `plugins.json` to use the published plugin:
```
{
"server": [
...
{"talk-plugin-asset-manager": "^0.1"}
],
...
}
```
Finally, run the `reconcile` script to install the plugin from npm.
```
$ bin/cli plugins reconcile
```
Once you've taken this step, anyone can register your plugin into their Talk server! Thank you for contributing to the open source community!
### Publish to version control
This plugin is open source, so I'm also going to [publish it to github](https://github.com/jde/talk-plugin-asset-manager/commit/66b626caa85cb8030b3ddaa7c1a4821bf01e350a) and [cut a release](https://github.com/jde/talk-plugin-asset-manager/releases/tag/v0.1) that mirrors the npm relese.
## Done!
+59
View File
@@ -189,6 +189,41 @@ send data to the client. If the type in question contains args, clients may subs
For more information, see the [Apollo Docs](https://github.com/apollographql/graphql-subscriptions).
#### Field: `tokenUserNotFound`
```js
tokenUserNotFound: async ({jwt, token}) => {
let profile = await someExternalService(token);
if (!profile) {
return null;
}
let user = await UserModel.findOneAndUpdate({
id: profile.id
}, {
id: profile.id,
username: profile.username,
lowercaseUsername: profile.username.toLowerCase(),
roles: [],
profiles: []
}, {
setDefaultsOnInsert: true,
new: true,
upsert: true
});
return user;
}
```
The `tokenUserNotFound` hook allows auth integrations to hook into the event
when a valid token is provided but a user can't be found in the database that
matches the provided id.
The function is async, and should return the user object that was created in the
database, or null if the user wasn't found. The `jwt` paramenter of the object
is the unpacked token, while `token` is the original jwt token string.
### Routes
#### Field: `router`
@@ -205,6 +240,30 @@ The Router hook allows you to create a function that accepts the base express
router where you can mount any amount of middleware/routes to do any form of
action needed by external applications.
#### Field: `tags`
The tags hook allows a plugin to define tags that are code controlled (added
or enabled by code). Below is an example pulled from the core off topic plugin
on how to create a hook for the `OFF_TOPIC` name:
```js
[
{
name: 'OFF_TOPIC',
permissions: {
public: true,
self: true,
roles: []
},
models: ['COMMENTS'],
created_at: new Date()
}
]
```
You can refer to `models/schema/tag.js` for the available schema to match when
creating models to enable/disable specific features.
### Authorization middleware
The following example creates the requisite callback route and passport
+105
View File
@@ -0,0 +1,105 @@
---
title: Plugins Overview
keywords: plugins
sidebar: talk_sidebar
permalink: plugins.html
summary:
---
## Plugin Registration
In order for a plugin to be active in a Talk install, it must be _registered_. The parsing order for the plugin registration is as follows:
- `TALK_PLUGINS_JSON` environment variable
- `plugins.json` file
- `plugins.default.json` file
If you need to "disable all plugins", you can simply provide `{}` as the
contents of `process.env.TALK_PLUGINS_JSON` or the `plugins.json`.
The format for this is thus:
```json
{
"server": [
"people"
]
}
```
Where we have a `server` key with an array of plugins that match the folder
name in the `plugins/` folder. For example, the above config would
require a plugin from `plugins/people`, which must provide a `index.js` file
that returns an object that matches the Plugin Specification.
If the package is external (available on NPM) you can specify the string for
the version by using an object instead, for example:
```json
{
"server": [
{"people": "^1.2.0"}
]
}
```
External plugins can be resolved by running:
```bash
./bin/cli plugins reconcile
```
This achieves two things:
1. It will traverse into local plugin folders and install their dependencies.
_Note that if the plugin is already installed and available in the node_modules folder, it will not be
fetched again unless there is a version mismatch._ This will result in the
project `package.json` and `yarn.lock` files to be modified, this is normal as
this ensures that repeated deployments (with the same config) will have the
same config, these changes should not be committed to source control.
2. It will seek out dependencies that are listed in the object notation and try
to install them from npm.
## Plugin Dependencies
You may also include additional external dependencies in your local packages by
specifying a `package.json` at your plugin root which will result in a
`node_modules` folder being generated at the plugin root with your specific
dependencies.
## Deployment Solutions
Plugins can be deployed with a production instance of Talk.
### Source
Source deployments can just modify the `plugins.json` file and include any
local plugins into the `plugins/` directory. After including the config, you
need to reconcile the plugins and build the static assets:
```bash
# get plugin dependancies and remote plugins
./bin/cli plugins reconcile
# build staic assets (including enabled client side plugins)
yarn build
```
Then the application can be started as is.
### Docker
If you deploy using Docker, you can extend from the `*-onbuild` image, an
example `Dockerfile` for your project could be:
```Dockerfile
FROM coralproject/talk:latest-onbuild
```
Where the directory for your instance would contain a `plugins.json` file
describing the plugin requirements and a `plugins` directory containing any
other local plugins that should be included.
Onbuild triggers will execute when the image is building with your custom
configuration and will ensure that the image is ready to use by building all
assets inside the image as well.