├── .deliver
└── config
├── .env_sample
├── .formatter.exs
├── .gitignore
├── README.md
├── assets
├── .babelrc
├── css
│ ├── app.css
│ └── phoenix.css
├── js
│ ├── app.js
│ └── socket.js
├── package-lock.json
├── package.json
├── static
│ ├── favicon.ico
│ ├── images
│ │ └── phoenix.png
│ └── robots.txt
└── webpack.config.js
├── config
├── config.exs
├── dev.exs
├── prod.exs
└── test.exs
├── lib
├── instaghub.ex
├── instaghub
│ ├── application.ex
│ ├── exception.ex
│ ├── ins.ex
│ ├── kv.ex
│ ├── pool_macro.ex
│ ├── proxy.ex
│ ├── redis.ex
│ ├── repo.ex
│ ├── schedule.ex
│ ├── schedule_ins.ex
│ └── utils.ex
├── instaghub_web.ex
├── instaghub_web
│ ├── channels
│ │ └── user_socket.ex
│ ├── controllers
│ │ └── page_controller.ex
│ ├── endpoint.ex
│ ├── gettext.ex
│ ├── plugs
│ │ ├── cache.ex
│ │ └── qps.ex
│ ├── router.ex
│ ├── seo.ex
│ ├── templates
│ │ ├── error
│ │ │ ├── 404.html
│ │ │ └── 429.html
│ │ ├── html
│ │ │ └── posts.html.eex
│ │ ├── layout
│ │ │ └── app.html.eex
│ │ └── page
│ │ │ ├── about.html.eex
│ │ │ ├── index.html.eex
│ │ │ ├── post_comment.html.eex
│ │ │ ├── privacy.html.eex
│ │ │ ├── search.html.eex
│ │ │ ├── tag.html.eex
│ │ │ └── user.html.eex
│ └── views
│ │ ├── error_helpers.ex
│ │ ├── error_view.ex
│ │ ├── html_view.ex
│ │ ├── layout_view.ex
│ │ └── page_view.ex
└── instagram
│ ├── ins_web_api.ex
│ ├── ins_web_model.ex
│ └── ins_web_parser.ex
├── mix.exs
├── mix.lock
├── nginx
└── instaghub.com
├── priv
├── gettext
│ ├── en
│ │ └── LC_MESSAGES
│ │ │ └── errors.po
│ └── errors.pot
└── repo
│ ├── migrations
│ └── .formatter.exs
│ └── seeds.exs
├── rel
├── config.exs
├── plugins
│ └── .gitignore
└── vm.args
└── test
├── instaghub_web
├── controllers
│ └── page_controller_test.exs
└── views
│ ├── error_view_test.exs
│ ├── layout_view_test.exs
│ └── page_view_test.exs
├── support
├── channel_case.ex
├── conn_case.ex
└── data_case.ex
└── test_helper.exs
/.deliver/config:
--------------------------------------------------------------------------------
1 | APP="instaghub"
2 |
3 | BUILD_HOST="instaghub1"
4 | BUILD_USER="root"
5 | BUILD_AT="/root/$APP/app_build"
6 |
7 | START_DEPLOY=true
8 | CLEAN_DEPLOY=true
9 |
10 | # prevent re-installing node modules; this defaults to "."
11 | GIT_CLEAN_PATHS="_build rel priv/static"
12 |
13 | PRODUCTION_HOSTS="instaghub1"
14 | PRODUCTION_USER="root"
15 | DELIVER_TO="/root/$APP/app_release"
16 |
17 | # For Phoenix projects, symlink prod.secret.exs to our tmp source
18 | pre_erlang_get_and_update_deps() {
19 | local _prod_secret_path="/root/$APP/app_config/prod.secret.exs"
20 | if [ "$TARGET_MIX_ENV" = "prod" ]; then
21 | status "Linking '$_prod_secret_path'"
22 | __sync_remote "
23 | [ -f ~/.profile ] && source ~/.profile
24 | mkdir -p '$BUILD_AT'
25 | ln -sfn '$_prod_secret_path' '$BUILD_AT/config/prod.secret.exs'
26 | "
27 | fi
28 | }
29 |
30 | pre_erlang_clean_compile() {
31 | status "Running npm install"
32 | __sync_remote "
33 | [ -f ~/.profile ] && source ~/.profile
34 | set -e
35 | cd '$BUILD_AT'/assets
36 | npm install
37 | "
38 |
39 | status "Compiling assets"
40 | __sync_remote "
41 | [ -f ~/.profile ] && source ~/.profile
42 | set -e
43 | cd '$BUILD_AT'/assets
44 | node_modules/.bin/webpack --mode production --silent
45 | "
46 |
47 | status "Running phoenix.digest" # log output prepended with "----->"
48 | __sync_remote " # runs the commands on the build host
49 | [ -f ~/.profile ] && source ~/.profile # load profile (optional)
50 | set -e # fail if any command fails (recommended)
51 | cd '$BUILD_AT' # enter the build directory on the build host (required)
52 | # prepare something
53 | mkdir -p priv/static # required by the phoenix.digest task
54 | # run your custom task
55 | APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phx.digest $SILENCE
56 | APP='$APP' MIX_ENV='$TARGET_MIX_ENV' $MIX_CMD phx.digest.clean $SILENCE
57 | "
58 | }
59 |
--------------------------------------------------------------------------------
/.env_sample:
--------------------------------------------------------------------------------
1 | export INS_SESSION_ID_SPORTS="15151447131%3AiLuwr50mWdXfsz%3A15"
2 | export INS_SESSION_ID_WOMEN="9240358772%3AXA3PJwFrcxi2IH%3A17"
3 | export INS_SESSION_ID_ANIMAL="9444426544%3ASSmXrdDYJt8IrY%3A0"
4 | export INS_SESSION_ID_GAME="11658585665%3AiJeECzOuOucZmy%3A6"
5 | export INS_SESSION_ID_FOOD="11470214398%3AmnKVpyIylVSm3E%3A23"
6 | export INS_SESSION_ID_HOT="11658769798%3AKI5EpFWvayXPOP%3A12"
7 | export INS_SESSION_ID_OTHER=11658769798%3AKI5EpFWvayXPOP%3A12,11470214398%3AmnKVpyIylVSm3E%3A23
8 | export INS_NOT_LOGIN=1
9 | export INS_GOOGLE_QPS=3
10 | export PROXYS=143.191.230.202:3199,181.214.193.243:3199,168.80.71.40:3199,168.81.103.84:3199,45.43.128.22:3199,196.18.178.96:3199,108.59.14.203:13010,168.80.228.90:3199,168.80.183.191:3199,168.81.230.87:3199,104.233.48.27:3199,108.59.14.203:13010,168.81.246.228:3199,168.80.38.95:3199,196.19.191.80:3199,168.81.38.77:3199,168.81.53.38:3199,108.59.14.203:13010,168.81.100.127:3199,168.80.70.119:3199,196.18.178.13:3199,196.16.95.164:3199,104.239.119.248:3199,168.80.149.204:3199,168.81.151.22:3199,108.59.14.203:13010,181.177.75.245:3199,168.81.71.41:3199,143.191.229.6:3199,168.81.215.148:3199,196.17.123.237:3199,143.191.233.76:3199,108.59.14.203:13010,168.81.20.102:3199,154.16.129.229:3199,168.81.5.181:3199,196.16.249.224:3199,181.215.62.114:3199,196.18.150.79:3199,168.80.37.135:3199,108.59.14.203:13010,196.16.222.77:3199,168.81.20.153:3199,196.18.252.131:3199,108.59.14.203:13010,191.96.198.229:3199,168.81.133.180:3199,181.214.194.129:3199,108.59.14.203:13010,186.179.25.143:3199,168.81.166.221:3199,181.214.197.230:3199,168.81.53.146:3199,67.227.120.64:3199,108.59.14.203:13010,168.80.36.92:3199,168.80.7.28:3199,168.81.198.177:3199,168.80.23.74:3199,108.59.14.203:13010
11 |
--------------------------------------------------------------------------------
/.formatter.exs:
--------------------------------------------------------------------------------
1 | [
2 | import_deps: [:ecto, :phoenix],
3 | inputs: ["*.{ex,exs}", "priv/*/seeds.exs", "{config,lib,test}/**/*.{ex,exs}"],
4 | subdirectories: ["priv/*/migrations"]
5 | ]
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Ignore package tarball (built via "mix hex.build").
23 | instaghub-*.tar
24 |
25 | # If NPM crashes, it generates a log, let's ignore it too.
26 | npm-debug.log
27 |
28 | # The directory NPM downloads your dependencies sources to.
29 | /assets/node_modules/
30 |
31 | # Since we are building assets from assets/,
32 | # we ignore priv/static. You may want to comment
33 | # this depending on your deployment strategy.
34 | /priv/static/
35 |
36 | # Files matching config/*.secret.exs pattern contain sensitive
37 | # data and you should not commit them into version control.
38 | #
39 | # Alternatively, you may comment the line below and commit the
40 | # secrets files as long as you replace their contents by environment
41 | # variables.
42 | /config/*.secret.exs
43 |
44 | .env
45 | .deliver/releases/
46 |
47 | .elixir_ls/
48 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Instaghub
2 |
3 | 
4 |
5 | To start your Phoenix server:
6 |
7 | * Install dependencies with `mix deps.get`
8 | * Create and migrate your database with `mix ecto.setup`
9 | * Install Node.js dependencies with `cd assets && npm install`
10 | * Start Phoenix endpoint with `mix phx.server`
11 |
12 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser.
13 |
14 | Ready to run in production? Please [check our deployment guides](https://hexdocs.pm/phoenix/deployment.html).
15 |
16 | ## Learn more
17 |
18 | * Official website: http://www.phoenixframework.org/
19 | * Guides: https://hexdocs.pm/phoenix/overview.html
20 | * Docs: https://hexdocs.pm/phoenix
21 | * Mailing list: http://groups.google.com/group/phoenix-talk
22 | * Source: https://github.com/phoenixframework/phoenix
23 |
24 | ## Deployment
25 | ### local src upload build server to compile
26 | mix edeliver build release production
27 | ### build server copy target app to production server
28 | mix edeliver deploy release to production
29 | ### start production server
30 | mix edeliver restart production
31 |
32 | ## Edeliver
33 | mix edeliver ping production # shows which nodes are up and running
34 |
35 | mix edeliver version production # shows the release version running on the nodes
36 |
37 | mix edeliver show migrations on production # shows pending database migrations
38 |
39 | mix edeliver migrate production # run database migrations
40 |
41 | mix edeliver restart production # or start or stop
42 |
--------------------------------------------------------------------------------
/assets/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/assets/css/app.css:
--------------------------------------------------------------------------------
1 | /* This file is for your main application css. */
2 |
3 | @import "./phoenix.css";
4 |
5 | * { box-sizing: border-box; }
6 |
7 | body { font-family: sans-serif; }
8 |
9 | /* ---- grid ---- */
10 |
11 | .container {
12 | max-width: 100%;
13 | }
14 |
15 | .grid {
16 | max-width: 100%;
17 | }
18 |
19 | /* clearfix */
20 | .grid:after {
21 | content: '';
22 | display: block;
23 | clear: both;
24 | }
25 |
26 | .grid-sizer { width: 33.333%; }
27 |
28 | /* ---- grid-item ---- */
29 |
30 | .grid-item {
31 | display: inline-block;
32 | vertical-align: top;
33 | width: 33%;
34 | background-color: #fff;
35 | overflow: hidden;
36 | box-shadow: 0 0 1px #e7e7e7;
37 | margin-bottom: 10px;
38 | padding: 20px;
39 | }
40 |
41 | @media (max-width: 684px) {
42 | .grid-item {
43 | width: 100%;
44 | padding: 0;
45 | }
46 | }
47 |
48 | .page {
49 | display: none;
50 | }
51 |
52 | .has-next-page {
53 | display: none;
54 | }
55 |
56 | .next-page {
57 | text-align: center;
58 | font-size: x-large;
59 | font-weight: 100;
60 | animation: blinker 1s linear infinite;
61 | padding-top: 20px;
62 | }
63 |
64 | @keyframes blinker {
65 | 50% {
66 | opacity: 0;
67 | }
68 | }
69 |
70 | .grid-media {
71 | position: relative;
72 | height: auto;
73 | }
74 |
75 | .grid-media-type {
76 | position: absolute;
77 | right: 15px;
78 | bottom: 15px;
79 | color: white;
80 | font-size: larger;
81 | }
82 |
83 | .grid-media-shadow {
84 | position: absolute;
85 | width: 100%;
86 | height: 100%;
87 | top: 0;
88 | left: 0;
89 | background-color: rgba(10, 10, 10, .4)
90 | }
91 |
92 | .video {
93 | font-size: -webkit-xxx-large;
94 | left: 0;
95 | top: 0;
96 | right: 0;
97 | bottom: 0;
98 | margin: auto;
99 | text-align: center;
100 | width: 150px;
101 | height: 100px;
102 | z-index: 10;
103 | }
104 |
105 | .button-warning {
106 | background: rgb(223, 117, 20); /* this is an orange */
107 | }
108 |
109 | .video-play-counts {
110 | font-size: medium;
111 | margin-left: -13%;
112 | margin-top: -5%;
113 | }
114 |
115 | .image {
116 | font-size: x-large;
117 | }
118 |
119 | .grid-caption {
120 | margin: 0;
121 | }
122 |
123 | .grid-caption > p {
124 | margin: 0;
125 | }
126 |
127 | .grid-stats {
128 | display: flex;
129 | font-size: small;
130 | justify-content: space-between;
131 | }
132 |
133 | .grid-stats-likes {
134 | margin-right: 2px;
135 | }
136 |
137 | .grid-stats-comments {
138 | margin-right: 2px;
139 | }
140 |
141 | .grid-stats-created {
142 | font-size: small;
143 | margin: 0;
144 | }
145 |
146 | .line{
147 | width: 100%;
148 | border-bottom: 1px solid #eee;
149 | margin-bottom: 5px;
150 | }
151 |
152 | .grid-stats-download {
153 | }
154 |
155 | /*navigation bar*/
156 | .nav {
157 | margin: auto;
158 | width: 96%;
159 | }
160 |
161 | .nav form {
162 | margin-bottom: 0px;
163 | }
164 |
165 | .custom-wrapper {
166 | /* background-color: #ffd390; */
167 | font-weight: 700;
168 | margin-bottom: 1em;
169 | -webkit-font-smoothing: antialiased;
170 | height: 2.1em;
171 | overflow: hidden;
172 | -webkit-transition: height 0.5s;
173 | -moz-transition: height 0.5s;
174 | -ms-transition: height 0.5s;
175 | transition: height 0.5s;
176 | }
177 |
178 | .custom-wrapper.open {
179 | height: 23em;
180 | }
181 |
182 | .custom-menu-3 {
183 | text-align: right;
184 | }
185 |
186 | .custom-toggle {
187 | width: 34px;
188 | height: 34px;
189 | position: absolute;
190 | top: 0;
191 | right: 0;
192 | display: none;
193 | }
194 |
195 | .custom-toggle .bar {
196 | background-color: #777;
197 | display: block;
198 | width: 20px;
199 | height: 2px;
200 | border-radius: 100px;
201 | position: absolute;
202 | top: 18px;
203 | right: 7px;
204 | -webkit-transition: all 0.5s;
205 | -moz-transition: all 0.5s;
206 | -ms-transition: all 0.5s;
207 | transition: all 0.5s;
208 | }
209 |
210 | .custom-toggle .bar:first-child {
211 | -webkit-transform: translateY(-6px);
212 | -moz-transform: translateY(-6px);
213 | -ms-transform: translateY(-6px);
214 | transform: translateY(-6px);
215 | }
216 |
217 | .custom-toggle.x .bar {
218 | -webkit-transform: rotate(45deg);
219 | -moz-transform: rotate(45deg);
220 | -ms-transform: rotate(45deg);
221 | transform: rotate(45deg);
222 | }
223 |
224 | .custom-toggle.x .bar:first-child {
225 | -webkit-transform: rotate(-45deg);
226 | -moz-transform: rotate(-45deg);
227 | -ms-transform: rotate(-45deg);
228 | transform: rotate(-45deg);
229 | }
230 |
231 | @media (max-width: 47.999em) {
232 | .custom-menu-3 {
233 | text-align: left;
234 | }
235 | .custom-toggle {
236 | display: block;
237 | }
238 | }
239 |
240 | /*post comment*/
241 | .post-comment-media {
242 | width: 100%;
243 | padding: 0;
244 | }
245 |
246 | .post-comment-media img {
247 | width: 100%;
248 | }
249 |
250 | .post-comment-media video {
251 | width: 100%;
252 | }
253 |
254 | .post-comment-info .owner {
255 | height: auto;
256 | margin-top: 20px;
257 | border: 1px solid #ddd;
258 | border-left: none;
259 | border-right: none;
260 | padding: 15px 0;
261 | }
262 |
263 | .post-comment-info .info {
264 | padding-left: 10px;
265 | }
266 |
267 | .post-comment-info .owner img {
268 | display: inline-block;
269 | width: 100px;
270 | height: 100px;
271 | vertical-align: middle;
272 | }
273 |
274 | .post-comment-info .owner p {
275 | display: block;
276 | margin: 0;
277 | }
278 |
279 | .post-comment-info .comments .head {
280 | margin: 20px 0;
281 | font-weight: bold;
282 | font-size: larger;
283 | }
284 |
285 | .post-comment-info .comments .comment-user {
286 | margin-bottom: 10px;
287 | }
288 |
289 | .post-comment-info .comments .comment-user img {
290 | width: 50px;
291 | height: 50px;
292 | }
293 |
294 | .post-comment-info .comments .comment-text {
295 | display: flex;
296 | flex-direction: column;
297 | margin-bottom: 10px;
298 | }
299 |
300 | @media (max-width: 684px) {
301 | .post-comment-info .owner img {
302 | width: 60px;
303 | height: 60px;
304 | }
305 | }
306 |
307 | .user-profile p {
308 | margin: 0;
309 | }
310 |
311 | .ads {
312 | min-width: 250px;
313 | min-height: 250px;
314 | max-height: 800px;
315 | height: auto;
316 | overflow:hidden;
317 | display:block;
318 | text-align:center;
319 | }
320 |
321 | /* hidden infolinks IL_IN_CONTENT AD */
322 | input[name="IL_IN_CONTENT"]+div {
323 | display: none !important;
324 | }
325 |
--------------------------------------------------------------------------------
/assets/css/phoenix.css:
--------------------------------------------------------------------------------
1 | /* Includes some default style for the starter application.
2 | * This can be safely deleted to start fresh.
3 | */
4 |
5 | /* Milligram v1.3.0 https://milligram.github.io
6 | * Copyright (c) 2017 CJ Patoilo Licensed under the MIT license
7 | */
8 |
9 | *,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#000000;font-family:'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#0069d9;border:0.1rem solid #0069d9;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#0069d9;border-color:#0069d9}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#0069d9}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#0069d9}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#0069d9}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#0069d9}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #0069d9;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='email'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],textarea,select{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem;width:100%}input[type='email']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,textarea:focus,select:focus{border-color:#0069d9;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{-ms-grid-row-align:center;align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#0069d9;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem;text-align:left}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right}
10 |
11 | /* General style */
12 | h1{font-size: 3.6rem; line-height: 1.25}
13 | h2{font-size: 2.8rem; line-height: 1.3}
14 | h3{font-size: 2.2rem; letter-spacing: -.08rem; line-height: 1.35}
15 | h4{font-size: 1.8rem; letter-spacing: -.05rem; line-height: 1.5}
16 | h5{font-size: 1.6rem; letter-spacing: 0; line-height: 1.4}
17 | h6{font-size: 1.4rem; letter-spacing: 0; line-height: 1.2}
18 |
19 | .container{
20 | margin: 0 auto;
21 | max-width: 80.0rem;
22 | padding: 0 2.0rem;
23 | position: relative;
24 | width: 100%
25 | }
26 | select {
27 | width: auto;
28 | }
29 |
30 | /* Alerts and form errors */
31 | .alert {
32 | padding: 15px;
33 | margin-bottom: 20px;
34 | border: 1px solid transparent;
35 | border-radius: 4px;
36 | }
37 | .alert-info {
38 | color: #31708f;
39 | background-color: #d9edf7;
40 | border-color: #bce8f1;
41 | }
42 | .alert-warning {
43 | color: #8a6d3b;
44 | background-color: #fcf8e3;
45 | border-color: #faebcc;
46 | }
47 | .alert-danger {
48 | color: #a94442;
49 | background-color: #f2dede;
50 | border-color: #ebccd1;
51 | }
52 | .alert p {
53 | margin-bottom: 0;
54 | }
55 | .alert:empty {
56 | display: none;
57 | }
58 | .help-block {
59 | color: #a94442;
60 | display: block;
61 | margin: -1rem 0 2rem;
62 | }
63 |
64 | /* Phoenix promo and logo */
65 | .phx-hero {
66 | text-align: center;
67 | border-bottom: 1px solid #e3e3e3;
68 | background: #eee;
69 | border-radius: 6px;
70 | padding: 3em;
71 | margin-bottom: 3rem;
72 | font-weight: 200;
73 | font-size: 120%;
74 | }
75 | .phx-hero p {
76 | margin: 0;
77 | }
78 | .phx-logo {
79 | min-width: 300px;
80 | margin: 1rem;
81 | display: block;
82 | }
83 | .phx-logo img {
84 | width: auto;
85 | display: block;
86 | }
87 |
88 | /* Headers */
89 | header {
90 | width: 100%;
91 | background: #fdfdfd;
92 | border-bottom: 1px solid #eaeaea;
93 | margin-bottom: 2rem;
94 | }
95 | header section {
96 | align-items: center;
97 | display: flex;
98 | flex-direction: column;
99 | justify-content: space-between;
100 | }
101 | header section :first-child {
102 | order: 2;
103 | }
104 | header section :last-child {
105 | order: 1;
106 | }
107 | header nav ul,
108 | header nav li {
109 | margin: 0;
110 | padding: 0;
111 | display: block;
112 | text-align: right;
113 | white-space: nowrap;
114 | }
115 | header nav ul {
116 | margin: 1rem;
117 | margin-top: 0;
118 | }
119 | header nav a {
120 | display: block;
121 | }
122 |
123 | @media (min-width: 40.0rem) { /* Small devices (landscape phones, 576px and up) */
124 | header section {
125 | flex-direction: row;
126 | }
127 | header nav ul {
128 | margin: 1rem;
129 | }
130 | .phx-logo {
131 | flex-basis: 527px;
132 | margin: 2rem 1rem;
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/assets/js/app.js:
--------------------------------------------------------------------------------
1 | // We need to import the CSS so that webpack will load it.
2 | // The MiniCssExtractPlugin is used to separate it out into
3 | // its own CSS file.
4 | import css from "../css/app.css";
5 |
6 | // webpack automatically bundles all modules in your
7 | // entry points. Those entry points can be configured
8 | // in "webpack.config.js".
9 | //
10 | // Import dependencies
11 | //
12 | import "phoenix_html";
13 | var Masonry = require('masonry-layout');
14 | var Imagesloaded = require('imagesloaded');
15 | var jQueryBridget = require('jquery-bridget');
16 | // make $ available in Chrome console
17 | $ = window.$ = window.jQuery = require("jquery");
18 | // make Masonry a jQuery plugin
19 | jQueryBridget( 'masonry', Masonry, $ );
20 | // provide jQuery argument
21 | Imagesloaded.makeJQueryPlugin( $ );
22 |
23 | var $grid = $('.grid').masonry({
24 | // options
25 | itemSelector: '.grid-item',
26 | // columnWidth: '.grid-sizer',
27 | gutter: 5,
28 | percentPosition: false,
29 | //horizontalOrder: true,
30 | isFitWidth: false,
31 | initLayout: false
32 | });
33 | // bind event
34 | $grid.masonry( 'on', 'layoutComplete', function() {
35 | //console.log('layout is complete');
36 | });
37 |
38 | // Import local files
39 | //
40 | // Local files can be imported directly using relative paths, for example:
41 | // import socket from "./socket"
42 | var is_get_new_page = 0;
43 | $(window).scroll(function() {
44 | if($(window).scrollTop() + $(window).height() + 100 >= ($(document).height()) && $("#load_next_page").length > 0) {
45 | // bottom to get new page
46 | if(is_get_new_page == 0) {
47 | $("#load_next_page").show();
48 | is_get_new_page = 1;
49 | var cursor = $(".page")[0].textContent;
50 | var has_next_page = $(".has-next-page")[0].textContent.trim();
51 | var url = window.location.pathname + "/?cursor=" + cursor;
52 | url = url.replace("//", "/");
53 | var $user_id = $(".user-id");
54 | if ($user_id.length > 0) {
55 | $user_id = $user_id[0].textContent;
56 | } else {
57 | $user_id = undefined;
58 | }
59 | if ($user_id !== undefined) {
60 | url = window.location.origin + url + "&id=" + $user_id;
61 | } else {
62 | url = window.location.origin + url;
63 | }
64 | // console.log("request path is " + url);
65 | if (has_next_page === "true") {
66 | $.ajax({
67 | url: url,
68 | success: function(res) {
69 | var $res = $(res);
70 | // store res in jquery data
71 | $('.grid').data("res", res);
72 | var $page = $res.filter('.page')[0];
73 | var $has_next_page = $res.filter('.has-next-page')[0];
74 | $(".page")[0].textContent = $page.textContent;
75 | $(".has-next-page")[0].textContent = $has_next_page.textContent;
76 | $grid.masonryImagesReveal($res, true);
77 | },
78 | timeout: 5000})
79 | .done(function() {
80 | // console.log($('.grid').data("res"));
81 | })
82 | .fail(function() {
83 | //console.log("error call ajax");
84 | })
85 | .always(function() {
86 | //console.log("reset nex page");
87 | is_get_new_page = 0;
88 | $("#load_next_page").hide();
89 | });
90 | } else {
91 | $("#load_next_page").hide();
92 | is_get_new_page = 0;
93 | }
94 | }
95 | }
96 | });
97 |
98 | $.fn.masonryImagesReveal = function($items, isAppend) {
99 | // hide by default
100 | $items.hide();
101 | // append to container
102 | if (isAppend == true) {
103 | $(".grid").append($items);
104 | $items.imagesLoaded()
105 | .progress(function(imgLoad, image) {
106 | // image is imagesLoaded class, not ,
is image.img
107 | var $item = $(image.img).parents(".grid-item");
108 | $item.show();
109 | $grid.masonry('appended', $item);
110 | // show next ads
111 | var $ads = $(image.img).parents(".grid-item").next().children(".ads");
112 | if ($ads.length!=0) {
113 | var $item_ads = $(image.img).parents(".grid-item").next();
114 | $item_ads.show();
115 | var adsbygoogle=(adsbygoogle = window.adsbygoogle || []);
116 | var $error = 0;
117 | $ads.children('ins').each(function(){
118 | try {
119 | adsbygoogle.push(this);
120 | } catch(error) {
121 | $error = 1;
122 | }
123 | });
124 | if ($error == 0) {
125 | $grid.masonry('appended', $item_ads);
126 | } else {
127 | $item_ads.hide();
128 | }
129 | }
130 | }).always( function( instance ){
131 | });
132 | } else {
133 | $items.imagesLoaded()
134 | .always( function( instance ) {
135 | // console.log('all images loaded');
136 | // trigger initial layout
137 | $items.show();
138 | $grid.masonry();
139 | })
140 | .done( function( instance ) {
141 | // console.log('all images successfully loaded');
142 | })
143 | .fail( function() {
144 | // console.log('all images loaded, at least one is broken');
145 | });
146 | }
147 | return this;
148 | };
149 |
150 | // first load to layout
151 | $(document).ready(function() {
152 | var $items = $('.grid-item');
153 | $grid.masonryImagesReveal($items, false);
154 | });
155 |
--------------------------------------------------------------------------------
/assets/js/socket.js:
--------------------------------------------------------------------------------
1 | // NOTE: The contents of this file will only be executed if
2 | // you uncomment its entry in "assets/js/app.js".
3 |
4 | // To use Phoenix channels, the first step is to import Socket,
5 | // and connect at the socket path in "lib/web/endpoint.ex".
6 | //
7 | // Pass the token on params as below. Or remove it
8 | // from the params if you are not using authentication.
9 | import {Socket} from "phoenix"
10 |
11 | let socket = new Socket("/socket", {params: {token: window.userToken}})
12 |
13 | // When you connect, you'll often need to authenticate the client.
14 | // For example, imagine you have an authentication plug, `MyAuth`,
15 | // which authenticates the session and assigns a `:current_user`.
16 | // If the current user exists you can assign the user's token in
17 | // the connection for use in the layout.
18 | //
19 | // In your "lib/web/router.ex":
20 | //
21 | // pipeline :browser do
22 | // ...
23 | // plug MyAuth
24 | // plug :put_user_token
25 | // end
26 | //
27 | // defp put_user_token(conn, _) do
28 | // if current_user = conn.assigns[:current_user] do
29 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id)
30 | // assign(conn, :user_token, token)
31 | // else
32 | // conn
33 | // end
34 | // end
35 | //
36 | // Now you need to pass this token to JavaScript. You can do so
37 | // inside a script tag in "lib/web/templates/layout/app.html.eex":
38 | //
39 | //
40 | //
41 | // You will need to verify the user token in the "connect/3" function
42 | // in "lib/web/channels/user_socket.ex":
43 | //
44 | // def connect(%{"token" => token}, socket, _connect_info) do
45 | // # max_age: 1209600 is equivalent to two weeks in seconds
46 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do
47 | // {:ok, user_id} ->
48 | // {:ok, assign(socket, :user, user_id)}
49 | // {:error, reason} ->
50 | // :error
51 | // end
52 | // end
53 | //
54 | // Finally, connect to the socket:
55 | socket.connect()
56 |
57 | // Now that you are connected, you can join channels with a topic:
58 | let channel = socket.channel("topic:subtopic", {})
59 | channel.join()
60 | .receive("ok", resp => { console.log("Joined successfully", resp) })
61 | .receive("error", resp => { console.log("Unable to join", resp) })
62 |
63 | export default socket
64 |
--------------------------------------------------------------------------------
/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "repository": {},
3 | "license": "MIT",
4 | "scripts": {
5 | "deploy": "webpack --mode production",
6 | "watch": "webpack --mode development --watch"
7 | },
8 | "dependencies": {
9 | "imagesloaded": "^4.1.4",
10 | "jquery": "^3.3.1",
11 | "jquery-bridget": "^2.0.1",
12 | "masonry-layout": "^4.2.2",
13 | "phoenix": "file:../deps/phoenix",
14 | "phoenix_html": "file:../deps/phoenix_html"
15 | },
16 | "devDependencies": {
17 | "@babel/core": "^7.0.0",
18 | "@babel/preset-env": "^7.0.0",
19 | "babel-loader": "^8.0.0",
20 | "copy-webpack-plugin": "^4.5.0",
21 | "css-loader": "^0.28.10",
22 | "mini-css-extract-plugin": "^0.4.0",
23 | "optimize-css-assets-webpack-plugin": "^4.0.0",
24 | "uglifyjs-webpack-plugin": "^1.2.4",
25 | "webpack": "4.4.0",
26 | "webpack-cli": "^3.2.3"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/assets/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmpi-dev/instaghub/da02821f082b16c59cfa07054efbb83303d7b9e0/assets/static/favicon.ico
--------------------------------------------------------------------------------
/assets/static/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bmpi-dev/instaghub/da02821f082b16c59cfa07054efbb83303d7b9e0/assets/static/images/phoenix.png
--------------------------------------------------------------------------------
/assets/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/assets/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const glob = require('glob');
3 | const MiniCssExtractPlugin = require('mini-css-extract-plugin');
4 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
5 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
6 | const CopyWebpackPlugin = require('copy-webpack-plugin');
7 |
8 | const webpack = require('webpack');
9 |
10 | module.exports = (env, options) => ({
11 | optimization: {
12 | minimizer: [
13 | new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false }),
14 | new OptimizeCSSAssetsPlugin({})
15 | ]
16 | },
17 | entry: {
18 | './js/app.js': ['./js/app.js'].concat(glob.sync('./vendor/**/*.js'))
19 | },
20 | output: {
21 | filename: 'app.js',
22 | path: path.resolve(__dirname, '../priv/static/js')
23 | },
24 | module: {
25 | rules: [
26 | {
27 | test: /\.js$/,
28 | exclude: /node_modules/,
29 | use: {
30 | loader: 'babel-loader'
31 | }
32 | },
33 | {
34 | test: /\.css$/,
35 | use: [MiniCssExtractPlugin.loader, 'css-loader']
36 | }
37 | ]
38 | },
39 | plugins: [
40 | new MiniCssExtractPlugin({ filename: '../css/app.css' }),
41 | new CopyWebpackPlugin([{ from: 'static/', to: '../' }]),
42 | new webpack.ProvidePlugin({
43 | $: "jquery",
44 | jQuery: "jquery"
45 | })
46 | ]
47 | });
48 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 |
7 | # General application configuration
8 | use Mix.Config
9 |
10 | config :instaghub,
11 | ecto_repos: [Instaghub.Repo]
12 |
13 | # Configures the endpoint
14 | config :instaghub, InstaghubWeb.Endpoint,
15 | url: [host: "localhost"],
16 | secret_key_base: "078VBgbhyElPf97JQ8GaWe8Cn0gwDFIEQ5H5p7ITAqAzsTUUAJKy0O0NH3Gzj89T",
17 | render_errors: [view: InstaghubWeb.ErrorView, accepts: ~w(html json)],
18 | pubsub: [name: Instaghub.PubSub, adapter: Phoenix.PubSub.PG2]
19 |
20 | # Configures Elixir's Logger
21 | config :logger, :console,
22 | format: "$time $metadata[$level] $message\n",
23 | metadata: [:request_id]
24 |
25 | # Use Jason for JSON parsing in Phoenix
26 | config :phoenix, :json_library, Jason
27 |
28 | # Import environment specific config. This must remain at the bottom
29 | # of this file so it overrides the configuration defined above.
30 | import_config "#{Mix.env()}.exs"
31 |
32 | config :instaghub,
33 | redis_uri: "redis://localhost:6379",
34 | redis_ttl: 31536000
35 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with webpack to recompile .js and .css sources.
9 | config :instaghub, InstaghubWeb.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | watchers: [
15 | node: [
16 | "node_modules/webpack/bin/webpack.js",
17 | "--mode",
18 | "development",
19 | "--watch-stdin",
20 | cd: Path.expand("../assets", __DIR__)
21 | ]
22 | ]
23 |
24 | # ## SSL Support
25 | #
26 | # In order to use HTTPS in development, a self-signed
27 | # certificate can be generated by running the following
28 | # Mix task:
29 | #
30 | # mix phx.gen.cert
31 | #
32 | # Note that this task requires Erlang/OTP 20 or later.
33 | # Run `mix help phx.gen.cert` for more information.
34 | #
35 | # The `http:` config above can be replaced with:
36 | #
37 | # https: [
38 | # port: 4001,
39 | # cipher_suite: :strong,
40 | # keyfile: "priv/cert/selfsigned_key.pem",
41 | # certfile: "priv/cert/selfsigned.pem"
42 | # ],
43 | #
44 | # If desired, both `http:` and `https:` keys can be
45 | # configured to run both http and https servers on
46 | # different ports.
47 |
48 | # Watch static and templates for browser reloading.
49 | config :instaghub, InstaghubWeb.Endpoint,
50 | live_reload: [
51 | patterns: [
52 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
53 | ~r{priv/gettext/.*(po)$},
54 | ~r{lib/instaghub_web/views/.*(ex)$},
55 | ~r{lib/instaghub_web/templates/.*(eex)$}
56 | ]
57 | ]
58 |
59 | # Do not include metadata nor timestamps in development logs
60 | config :logger, :console, format: "[$level] $message\n"
61 |
62 | # Set a higher stacktrace during development. Avoid configuring such
63 | # in production as building large stacktraces may be expensive.
64 | config :phoenix, :stacktrace_depth, 20
65 |
66 | # Initialize plugs at runtime for faster development compilation
67 | config :phoenix, :plug_init_mode, :runtime
68 |
69 | # Configure your database
70 | config :instaghub, Instaghub.Repo,
71 | username: "postgres",
72 | password: "postgres",
73 | database: "instaghub_dev",
74 | hostname: "localhost",
75 | pool_size: 10
76 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, don't forget to configure the url host
4 | # to something meaningful, Phoenix uses this information
5 | # when generating URLs.
6 | #
7 | # Note we also include the path to a cache manifest
8 | # containing the digested version of static files. This
9 | # manifest is generated by the `mix phx.digest` task,
10 | # which you should run after static files are built and
11 | # before starting your production server.
12 | config :instaghub, InstaghubWeb.Endpoint,
13 | http: [:inet6, port: System.get_env("PORT") || 4000],
14 | url: [host: "instaghub.com", port: 80],
15 | cache_static_manifest: "priv/static/cache_manifest.json",
16 | server: true,
17 | code_reloader: false
18 |
19 | # Do not print debug messages in production
20 | config :logger, level: :info
21 |
22 | # ## SSL Support
23 | #
24 | # To get SSL working, you will need to add the `https` key
25 | # to the previous section and set your `:url` port to 443:
26 | #
27 | # config :instaghub, InstaghubWeb.Endpoint,
28 | # ...
29 | # url: [host: "example.com", port: 443],
30 | # https: [
31 | # :inet6,
32 | # port: 443,
33 | # cipher_suite: :strong,
34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")
36 | # ]
37 | #
38 | # The `cipher_suite` is set to `:strong` to support only the
39 | # latest and more secure SSL ciphers. This means old browsers
40 | # and clients may not be supported. You can set it to
41 | # `:compatible` for wider support.
42 | #
43 | # `:keyfile` and `:certfile` expect an absolute path to the key
44 | # and cert in disk or a relative path inside priv, for example
45 | # "priv/ssl/server.key". For all supported SSL configuration
46 | # options, see https://hexdocs.pm/plug/Plug.SSL.html#configure/1
47 | #
48 | # We also recommend setting `force_ssl` in your endpoint, ensuring
49 | # no data is ever sent via http, always redirecting to https:
50 | #
51 | # config :instaghub, InstaghubWeb.Endpoint,
52 | # force_ssl: [hsts: true]
53 | #
54 | # Check `Plug.SSL` for all available options in `force_ssl`.
55 |
56 | # ## Using releases (distillery)
57 | #
58 | # If you are doing OTP releases, you need to instruct Phoenix
59 | # to start the server for all endpoints:
60 | #
61 | # config :phoenix, :serve_endpoints, true
62 | #
63 | # Alternatively, you can configure exactly which server to
64 | # start per endpoint:
65 | #
66 | # config :instaghub, InstaghubWeb.Endpoint, server: true
67 | #
68 | # Note you can't rely on `System.get_env/1` when using releases.
69 | # See the releases documentation accordingly.
70 |
71 | # Finally import the config/prod.secret.exs which should be versioned
72 | # separately.
73 | import_config "prod.secret.exs"
74 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :instaghub, InstaghubWeb.Endpoint,
6 | http: [port: 4002],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
12 | # Configure your database
13 | config :instaghub, Instaghub.Repo,
14 | username: "postgres",
15 | password: "postgres",
16 | database: "instaghub_test",
17 | hostname: "localhost",
18 | pool: Ecto.Adapters.SQL.Sandbox
19 |
--------------------------------------------------------------------------------
/lib/instaghub.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub do
2 | @moduledoc """
3 | Instaghub keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/lib/instaghub/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.Application do
2 | # See https://hexdocs.pm/elixir/Application.html
3 | # for more information on OTP Applications
4 | @moduledoc false
5 |
6 | use Application
7 |
8 | defp poolboy_config_ins do
9 | [
10 | {:name, {:local, :ins_api_pool}},
11 | {:worker_module, Instaghub.Ins},
12 | {:size, 5}
13 | ]
14 | end
15 |
16 | defp poolboy_config_redis do
17 | [
18 | {:name, {:local, :redis_pool}},
19 | {:worker_module, Instaghub.RedisUtil},
20 | {:size, 5},
21 | {:max_overflow, 2}
22 | ]
23 | end
24 |
25 | def start(_type, _args) do
26 | # List all child processes to be supervised
27 | children = [
28 | # Start the Ecto repository
29 | Instaghub.Repo,
30 | # Start the endpoint when the application starts
31 | InstaghubWeb.Endpoint,
32 | # Starts a worker by calling: Instaghub.Worker.start_link(arg)
33 | # {Instaghub.Worker, arg},
34 | Instaghub.Bucket,
35 | Instaghub.Bucket.Schedule,
36 | :poolboy.child_spec(:ins_api_pool, poolboy_config_ins()),
37 | :poolboy.child_spec(:redis_pool, poolboy_config_redis()),
38 | Instaghub.INS.Schedule,
39 | Instaghub.Proxy
40 | ]
41 |
42 | # See https://hexdocs.pm/elixir/Supervisor.html
43 | # for other strategies and supported options
44 | opts = [strategy: :one_for_one, name: Instaghub.Supervisor]
45 | Supervisor.start_link(children, opts)
46 | end
47 |
48 | # Tell Phoenix to update the endpoint configuration
49 | # whenever the application is updated.
50 | def config_change(changed, _new, removed) do
51 | InstaghubWeb.Endpoint.config_change(changed, removed)
52 | :ok
53 | end
54 | end
55 |
--------------------------------------------------------------------------------
/lib/instaghub/exception.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.Error429 do
2 | defexception [:code, :message]
3 | end
4 |
5 | defmodule Instaghub.Error404 do
6 | defexception [:code, :message]
7 | end
8 |
9 | defmodule Instaghub.ErrorOther do
10 | defexception [:message]
11 | end
12 |
--------------------------------------------------------------------------------
/lib/instaghub/ins.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.Ins do
2 | use GenServer
3 | alias Ins.Web.API
4 | require Instaghub.PoolMacro
5 |
6 | # Client
7 |
8 | def start_link(opts \\ []) do
9 | GenServer.start_link(__MODULE__, opts, [])
10 | end
11 |
12 | def get_feeds(cursor \\ nil) do
13 | Instaghub.PoolMacro.pool :ins_api_pool, opt: {:get_feeds, cursor}
14 | end
15 |
16 | def get_user_posts(id, cursor \\ nil) do
17 | Instaghub.PoolMacro.pool :ins_api_pool, opt: {:get_user_posts, {id, cursor}}
18 | end
19 |
20 | def get_tag_posts(tag_name, cursor \\ nil) do
21 | Instaghub.PoolMacro.pool :ins_api_pool, opt: {:get_tag_posts, {tag_name, cursor}}
22 | end
23 |
24 | def get_post_comment(shortcode) do
25 | Instaghub.PoolMacro.pool :ins_api_pool, opt: {:get_post_comment, shortcode}
26 | end
27 |
28 | def get_user_profile(user) do
29 | Instaghub.PoolMacro.pool :ins_api_pool, opt: {:get_user_profile, user}
30 | end
31 |
32 | def search_tags_users(query_str) do
33 | Instaghub.PoolMacro.pool :ins_api_pool, opt: {:search_tags_users, query_str}
34 | end
35 |
36 | # Server (callbacks)
37 |
38 | @impl true
39 | def init(config) do
40 | {:ok, config}
41 | end
42 |
43 | @impl true
44 | def handle_call(cmd, _from, stat) do
45 | res = case cmd do
46 | {:get_feeds, cursor} -> API.get_feeds(cursor)
47 | {:get_user_posts, {id, cursor}} -> API.get_user_posts(id, cursor)
48 | {:get_tag_posts, {tag_name, cursor}} -> API.get_tag_posts(tag_name, cursor)
49 | {:get_post_comment, shortcode} -> API.get_post_comment(shortcode)
50 | {:get_user_profile, user} -> API.get_user_profile(user)
51 | {:search_tags_users, query_str} -> API.search_tags_users(query_str)
52 | end
53 | {:reply, res, stat}
54 | end
55 |
56 | end
57 |
--------------------------------------------------------------------------------
/lib/instaghub/kv.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.Bucket do
2 | use Agent
3 | require Logger
4 |
5 | @name :kv
6 |
7 | @doc """
8 | Starts a new bucket.
9 | """
10 | def start_link(_opts) do
11 | Agent.start_link(fn -> %{} end, name: @name)
12 | end
13 |
14 | def increase_req(key) do
15 | Logger.debug "increase #{key} req"
16 | Agent.update(@name, fn map ->
17 | map_list = for {^key, v} <- map, do: Map.put(map, key, v + 1)
18 | map_list |> Enum.at(0)
19 | end)
20 | end
21 |
22 | def decrease_req(key) do
23 | Logger.debug "decrease #{key} req"
24 | Agent.update(@name, fn map ->
25 | map_list = for {^key, v} <- map do
26 | if v < 1 do
27 | Map.put(map, key, 0)
28 | else
29 | Map.put(map, key, v - 1)
30 | end
31 | end
32 | map_list |> Enum.at(0)
33 | end)
34 | end
35 |
36 | def reset_req(key) do
37 | req = get(key)
38 | # Logger.debug "reset #{key} with 0, before is #{req}"
39 | put(key, 0)
40 | end
41 |
42 | def get_req(key) do
43 | req = get(key)
44 | Logger.debug "req #{key} is #{req}"
45 | req
46 | end
47 |
48 | @doc """
49 | Gets a value from the `bucket` by `key`.
50 | """
51 | def get(key) do
52 | Agent.get(@name, &Map.get(&1, key))
53 | end
54 |
55 | @doc """
56 | Puts the `value` for the given `key` in the `bucket`.
57 | """
58 | def put(key, value) do
59 | Agent.update(@name, &Map.put(&1, key, value))
60 | end
61 |
62 | @doc """
63 | Deletes `key` from `bucket`.
64 |
65 | Returns the current value of `key`, if `key` exists.
66 | """
67 | def delete(key) do
68 | Agent.get_and_update(@name, &Map.pop(&1, key))
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/instaghub/pool_macro.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.PoolMacro do
2 | defmacro pool(name, opt: opt) do
3 | quote do
4 | :poolboy.transaction(
5 | unquote(name),
6 | fn pid -> GenServer.call(pid, unquote(opt)) end,
7 | 60_000)
8 | end
9 | end
10 | end
11 |
--------------------------------------------------------------------------------
/lib/instaghub/proxy.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.Proxy do
2 | use GenServer
3 | require Logger
4 |
5 | @name :proxy
6 |
7 | def start_link(_opts) do
8 | GenServer.start_link(__MODULE__, 0, name: @name)
9 | end
10 |
11 | def get() do
12 | GenServer.call(@name, :get)
13 | end
14 |
15 | def init(args) do
16 | {:ok, args}
17 | end
18 |
19 | def handle_call(:get, _from, index) do
20 | proxies = if System.get_env("PROXYS") != nil do
21 | System.get_env("PROXYS") |> String.split(",")
22 | else
23 | nil
24 | end
25 | if proxies != nil do
26 | len = length(proxies)
27 | index = rem(index, len)
28 | {:reply, proxies |> Enum.at(index), index+1}
29 | else
30 | {:reply, nil, 0}
31 | end
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/lib/instaghub/redis.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.RedisUtil do
2 | use GenServer
3 | require Instaghub.PoolMacro
4 |
5 | @redis_uri Application.get_env(:instaghub, :redis_uri)
6 | @redis_ttl Application.get_env(:instaghub, :redis_ttl)
7 |
8 | # Client
9 |
10 | def start_link(opts \\ []) do
11 | GenServer.start_link(__MODULE__, opts, [])
12 | end
13 |
14 | def get(key) do
15 | res = command(["GET", key])
16 | case res do
17 | {:ok, value} -> value
18 | _ -> nil
19 | end
20 | end
21 |
22 | def setx(key, value, ttl \\ nil) do
23 | ttl = if ttl == nil do
24 | @redis_ttl
25 | else
26 | ttl
27 | end
28 | r = command(["SET", key, value, "ex", ttl])
29 | case r do
30 | {:ok, "OK"} -> :ok
31 | _ -> :error
32 | end
33 | end
34 |
35 | def set(key, value) do
36 | r = command(["SET", key, value])
37 | case r do
38 | {:ok, "OK"} -> :ok
39 | _ -> :error
40 | end
41 | end
42 |
43 | defp command(cmd) do
44 | Instaghub.PoolMacro.pool :redis_pool, opt: {:cmd, cmd}
45 | end
46 |
47 | # Server (callbacks)
48 |
49 | @impl true
50 | def init(_config) do
51 | res = Redix.start_link(@redis_uri)
52 | case res do
53 | {:ok, conn} -> {:ok, conn}
54 | {:error, {:already_started, conn}} -> {:ok, conn}
55 | end
56 | end
57 |
58 | @impl true
59 | def handle_call({:cmd, command_list}, _from, conn) do
60 | res = Redix.command(conn, command_list)
61 | {:reply, res, conn}
62 | end
63 |
64 | end
65 |
--------------------------------------------------------------------------------
/lib/instaghub/repo.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.Repo do
2 | use Ecto.Repo,
3 | otp_app: :instaghub,
4 | adapter: Ecto.Adapters.Postgres
5 | end
6 |
--------------------------------------------------------------------------------
/lib/instaghub/schedule.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.Bucket.Schedule do
2 | use GenServer
3 |
4 | def start_link(_opts) do
5 | GenServer.start_link(__MODULE__, %{})
6 | end
7 |
8 | def init(state) do
9 | schedule_work() # Schedule work to be performed at some point
10 | {:ok, state}
11 | end
12 |
13 | def handle_info(:work, state) do
14 | # Do the work you desire here
15 | schedule_work() # Reschedule once more
16 | {:noreply, state}
17 | end
18 |
19 | defp schedule_work() do
20 | Instaghub.Bucket.reset_req(:googlebot)
21 | Instaghub.Bucket.reset_req(:otherbot)
22 | Instaghub.Bucket.reset_req(:human)
23 | Process.send_after(self(), :work, 10 * 1000)
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/instaghub/schedule_ins.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.INS.Schedule do
2 | use GenServer
3 |
4 | alias Instaghub.RedisUtil
5 | require Logger
6 |
7 | @ttl 60 * 60 * 12
8 | @rhx_gis "rhx_gis"
9 | @csrf "csrf"
10 |
11 | def start_link(_opts) do
12 | GenServer.start_link(__MODULE__, %{})
13 | end
14 |
15 | def init(state) do
16 | schedule_work() # Schedule work to be performed at some point
17 | {:ok, state}
18 | end
19 |
20 | def handle_info(:work, state) do
21 | # Do the work you desire here
22 | schedule_work() # Reschedule once more
23 | {:noreply, state}
24 | end
25 |
26 | defp schedule_work() do
27 | set_ins_token()
28 | Process.send_after(self(), :work, (@ttl - 60 * 20) * 1000)
29 | end
30 |
31 | defp set_ins_token() do
32 | user_agent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36"
33 | headers = [referer: "https://www.instagram.com/", "user-agent": user_agent]
34 | base_url = 'https://www.instagram.com'
35 | res = HTTPoison.get!(base_url, headers)
36 | re = ~r'_sharedData = .*?;'
37 | [[shared_data_json|_]|_] = Regex.scan(re, res.body)
38 | shared_data = shared_data_json |> String.replace("_sharedData =", "") |> String.replace(";", "") |> Poison.decode!
39 | rhx_gis = shared_data |> Map.get("rhx_gis")
40 | csrf = shared_data |> Map.get("config") |> Map.get("csrf_token")
41 | if rhx_gis != nil do
42 | RedisUtil.setx(@rhx_gis, rhx_gis, @ttl)
43 | Logger.debug "get rhx_gis and store in redis with key rhx_gis"
44 | end
45 | if csrf != nil do
46 | RedisUtil.setx(@csrf, csrf, @ttl)
47 | Logger.debug "get csrf and store in redis with key csrf"
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/instaghub/utils.ex:
--------------------------------------------------------------------------------
1 | defmodule Instaghub.Utils do
2 | alias Phoenix.HTML
3 | require Logger
4 |
5 | def md5_base64(str) do
6 | :crypto.hash(:md5, str)
7 | |> Base.encode64()
8 | end
9 |
10 | def parse_link(str) do
11 | str
12 | |> String.downcase
13 | |> parse_user_link
14 | |> parse_hashtag_link
15 | |> HTML.raw
16 | end
17 |
18 | defp parse_user_link(str) do
19 | str
20 | |> (fn(x) -> Regex.replace(~r/[@][^\s#@$!?]+/, x, "\\0", global: true) end).()
21 | |> String.replace("/user/@", "/user/")
22 | |> String.replace(".\">", "\">")
23 | |> String.replace(")\">", "\">")
24 | |> String.replace(" \">", "\">")
25 | end
26 |
27 | defp parse_hashtag_link(str) do
28 | str
29 | |> (fn(x) -> Regex.replace(~r/[#][^\s#@$!?]+/, x, "\\0", global: true) end).()
30 | |> String.replace("/tag/#", "/tag/")
31 | |> String.replace(".\">", "\">")
32 | |> String.replace(")\">", "\">")
33 | |> String.replace(" \">", "\">")
34 | end
35 |
36 | def check_ua_type(conn) do
37 | ua = conn |> Plug.Conn.get_req_header("user-agent") |> Enum.at(0)
38 | cond do
39 | is_googlebot(ua) ->
40 | :googlebot
41 | is_otherbot(ua) ->
42 | :otherbot
43 | true ->
44 | :human
45 | end
46 | end
47 |
48 | defp is_googlebot(ua) do
49 | case ua do
50 | nil -> false
51 | _ -> ua |> String.downcase |> (fn s -> String.contains?(s, "googlebot") end).()
52 | end
53 | end
54 |
55 | defp is_otherbot(ua) do
56 | other_bot = ["grapeshot", "ia_archiver", "slurp", "teoma", "yandex", "yeti", "baiduspider", "bot"]
57 | case ua do
58 | nil -> true
59 | _ -> ua |> String.downcase |> (fn s -> !String.contains?(s, "googlebot") && Enum.any?(Enum.map(other_bot, fn x -> String.contains?(s, x) end)) end).()
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/instaghub_web.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use InstaghubWeb, :controller
9 | use InstaghubWeb, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def controller do
21 | quote do
22 | use Phoenix.Controller, namespace: InstaghubWeb
23 |
24 | import Plug.Conn
25 | import InstaghubWeb.Gettext
26 | alias InstaghubWeb.Router.Helpers, as: Routes
27 | end
28 | end
29 |
30 | def view do
31 | quote do
32 | use Phoenix.View,
33 | root: "lib/instaghub_web/templates",
34 | namespace: InstaghubWeb
35 |
36 | # Import convenience functions from controllers
37 | import Phoenix.Controller, only: [get_flash: 1, get_flash: 2, view_module: 1]
38 |
39 | # Use all HTML functionality (forms, tags, etc)
40 | use Phoenix.HTML
41 |
42 | import InstaghubWeb.ErrorHelpers
43 | import InstaghubWeb.Gettext
44 | alias InstaghubWeb.Router.Helpers, as: Routes
45 | end
46 | end
47 |
48 | def router do
49 | quote do
50 | use Phoenix.Router
51 | import Plug.Conn
52 | import Phoenix.Controller
53 | end
54 | end
55 |
56 | def channel do
57 | quote do
58 | use Phoenix.Channel
59 | import InstaghubWeb.Gettext
60 | end
61 | end
62 |
63 | @doc """
64 | When used, dispatch to the appropriate controller/view/etc.
65 | """
66 | defmacro __using__(which) when is_atom(which) do
67 | apply(__MODULE__, which, [])
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/instaghub_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | # channel "room:*", InstaghubWeb.RoomChannel
6 |
7 | # Socket params are passed from the client and can
8 | # be used to verify and authenticate a user. After
9 | # verification, you can put default assigns into
10 | # the socket that will be set for all channels, ie
11 | #
12 | # {:ok, assign(socket, :user_id, verified_user_id)}
13 | #
14 | # To deny connection, return `:error`.
15 | #
16 | # See `Phoenix.Token` documentation for examples in
17 | # performing token verification on connect.
18 | def connect(_params, socket, _connect_info) do
19 | {:ok, socket}
20 | end
21 |
22 | # Socket id's are topics that allow you to identify all sockets for a given user:
23 | #
24 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
25 | #
26 | # Would allow you to broadcast a "disconnect" event and terminate
27 | # all active sockets and channels for a given user:
28 | #
29 | # InstaghubWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
30 | #
31 | # Returning `nil` makes this socket anonymous.
32 | def id(_socket), do: nil
33 | end
34 |
--------------------------------------------------------------------------------
/lib/instaghub_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb.PageController do
2 | use InstaghubWeb, :controller
3 | alias Ins.Web.API
4 | alias Instaghub.RedisUtil
5 | alias InstaghubWeb.Plug.Cache
6 | alias InstaghubWeb.SEO
7 | alias Instaghub.Utils
8 | require Logger
9 |
10 | defp before_halt(conn, ua_type) do
11 | Logger.debug "before halt we will decrease req #{ua_type}"
12 | Instaghub.Bucket.decrease_req(ua_type)
13 | conn
14 | end
15 |
16 | defp is_4xx(res) do
17 | case res do
18 | 429 -> true
19 | 404 -> true
20 | nil -> true
21 | _ -> false
22 | end
23 | end
24 |
25 | defp handle_4xx(conn, err_type) do
26 | ua_type = conn
27 | |> Utils.check_ua_type
28 | case ua_type do
29 | :googlebot -> googlebot_4xx_ac(conn, ua_type, err_type)
30 | :otherbot -> otherbot_4xx_ac(conn, ua_type, err_type)
31 | _ -> human_4xx_ac(conn, ua_type, err_type)
32 | end
33 | end
34 |
35 | defp googlebot_4xx_ac(conn, ua_type, err_type) do
36 | case err_type do
37 | 429 ->
38 | handle_429(conn, ua_type)
39 | _ ->
40 | handle_429(conn, ua_type)
41 | end
42 | end
43 |
44 | defp otherbot_4xx_ac(conn, ua_type, _err_type) do
45 | handle_429(conn, ua_type)
46 | end
47 |
48 | defp human_4xx_ac(conn, _ua_type, _err_type) do
49 | conn
50 | |> Phoenix.Controller.redirect(to: "/")
51 | #|> before_halt(ua_type)
52 | |> Plug.Conn.halt
53 | end
54 |
55 | defp handle_429(conn, ua_type) do
56 | conn
57 | |> Plug.Conn.put_status(:too_many_requests)
58 | |> Phoenix.Controller.put_view(InstaghubWeb.ErrorView)
59 | |> Phoenix.Controller.render("429.html", %{})
60 | |> before_halt(ua_type)
61 | |> Plug.Conn.halt
62 | end
63 |
64 | defp handle_404(conn, ua_type) do
65 | conn
66 | |> Plug.Conn.put_status(404)
67 |
68 | |> Phoenix.Controller.put_view(InstaghubWeb.ErrorView)
69 | |> Phoenix.Controller.render("404.html", %{})
70 | |> before_halt(ua_type)
71 | |> Plug.Conn.halt
72 | end
73 |
74 | def not_found(conn, _params) do
75 | handle_4xx(conn, 404)
76 | end
77 |
78 | def index(%Plug.Conn{request_path: path} = conn, _params) do
79 | cursor = Cache.get_cursor(conn)
80 | redis_key_md5 = Cache.get_page_key(conn, cursor)
81 | page = Cache.get_page(conn)
82 | feeds_with_page =
83 | if page == nil do
84 | feeds_with_page = case path do
85 | "/" -> API.get_feeds(cursor, :sports)
86 | "/explore/women" -> API.get_feeds(cursor, :women)
87 | "/explore/women/" -> API.get_feeds(cursor, :women)
88 | "/explore/animal" -> API.get_feeds(cursor, :animal)
89 | "/explore/animal/" -> API.get_feeds(cursor, :animal)
90 | "/explore/game" -> API.get_feeds(cursor, :game)
91 | "/explore/game/" -> API.get_feeds(cursor, :game)
92 | "/explore/food" -> API.get_feeds(cursor, :food)
93 | "/explore/food/" -> API.get_feeds(cursor, :food)
94 | "/explore/hot" -> API.get_feeds(cursor, :hot)
95 | "/explore/hot/" -> API.get_feeds(cursor, :hot)
96 | _ -> API.get_feeds(cursor)
97 | end
98 | if feeds_with_page != nil do
99 | feeds_bin = :erlang.term_to_binary(feeds_with_page)
100 | RedisUtil.setx(redis_key_md5, feeds_bin)
101 | Logger.debug "get page with api and store in redis with key #{redis_key_md5}"
102 | end
103 | feeds_with_page
104 | else
105 | page
106 | end
107 | if is_4xx(feeds_with_page) do
108 | handle_4xx(conn, feeds_with_page)
109 | else
110 | if System.get_env("INS_NOT_LOGIN") == "1" do
111 | get_tag(conn, feeds_with_page, cursor)
112 | else
113 | get_index(conn, path, feeds_with_page, cursor)
114 | end
115 | end
116 | end
117 |
118 | defp get_index(conn, path, feeds_with_page, cursor) do
119 | if cursor == nil do
120 | render(conn, "index.html", posts: feeds_with_page.posts, page_info: feeds_with_page.page_info, seo: SEO.get_index_seo(path, feeds_with_page))
121 | else
122 | conn
123 | |> put_layout(false)
124 | |> put_view(InstaghubWeb.HtmlView)
125 | |> render("posts.html", posts: feeds_with_page.posts, page_info: feeds_with_page.page_info)
126 | end
127 | end
128 |
129 | def post_comment(conn, %{"shortcode" => shortcode} = _params) do
130 | redis_key_md5 = Cache.get_page_key(conn, nil)
131 | page = Cache.get_page(conn)
132 | feeds_with_page =
133 | if page == nil do
134 | feeds_with_page = API.get_post_comment(shortcode)
135 | if feeds_with_page != nil do
136 | feeds_bin = :erlang.term_to_binary(feeds_with_page)
137 | RedisUtil.setx(redis_key_md5, feeds_bin)
138 | Logger.debug "get page with api and store in redis with key #{redis_key_md5}"
139 | end
140 | feeds_with_page
141 | else
142 | page
143 | end
144 | if is_4xx(feeds_with_page) do
145 | handle_4xx(conn, feeds_with_page)
146 | else
147 | render(conn, "post_comment.html", post: feeds_with_page, seo: SEO.get_post_seo(feeds_with_page))
148 | end
149 | end
150 |
151 | def user_posts(conn, %{"username" => username} = params) do
152 | id = Map.get(params, "id")
153 | cursor = Cache.get_cursor(conn)
154 | redis_key_md5 = Cache.get_page_key(conn, cursor)
155 | page = Cache.get_page(conn)
156 | feeds_with_page =
157 | if page == nil do
158 | feeds_with_page = if cursor == nil && id == nil do
159 | API.get_user_profile(username)
160 | else
161 | API.get_user_posts(id, cursor)
162 | end
163 | if feeds_with_page != nil do
164 | feeds_bin = :erlang.term_to_binary(feeds_with_page)
165 | RedisUtil.setx(redis_key_md5, feeds_bin)
166 | Logger.debug "get page with api and store in redis with key #{redis_key_md5}"
167 | end
168 | feeds_with_page
169 | else
170 | page
171 | end
172 | if is_4xx(feeds_with_page) do
173 | handle_4xx(conn, feeds_with_page)
174 | else
175 | if cursor == nil do
176 | render(conn, "user.html", posts: feeds_with_page.edge_owner_to_timeline_media.posts, page_info: feeds_with_page.edge_owner_to_timeline_media.page_info, user: feeds_with_page, seo: SEO.get_user_seo(feeds_with_page))
177 | else
178 | conn
179 | |> put_layout(false)
180 | |> put_view(InstaghubWeb.HtmlView)
181 | |> render("posts.html", posts: feeds_with_page.posts, page_info: feeds_with_page.page_info)
182 | end
183 | end
184 | end
185 |
186 | def tag_posts(conn, %{"tagname" => tagname} = _params) do
187 | cursor = Cache.get_cursor(conn)
188 | redis_key_md5 = Cache.get_page_key(conn, cursor)
189 | page = Cache.get_page(conn)
190 | feeds_with_page =
191 | if page == nil do
192 | feeds_with_page = API.get_tag_posts(tagname, cursor)
193 | if feeds_with_page != nil do
194 | feeds_bin = :erlang.term_to_binary(feeds_with_page)
195 | RedisUtil.setx(redis_key_md5, feeds_bin)
196 | Logger.debug "get page with api and store in redis with key #{redis_key_md5}"
197 | end
198 | feeds_with_page
199 | else
200 | page
201 | end
202 | if is_4xx(feeds_with_page) do
203 | handle_4xx(conn, feeds_with_page)
204 | else
205 | get_tag(conn, feeds_with_page, cursor)
206 | end
207 | end
208 |
209 | defp get_tag(conn, feeds_with_page, cursor) do
210 | if cursor == nil do
211 | render(conn, "tag.html", posts: feeds_with_page.edge_hashtag_to_media.posts, page_info: feeds_with_page.edge_hashtag_to_media.page_info, tag: feeds_with_page, seo: SEO.get_tag_seo(feeds_with_page))
212 | else
213 | conn
214 | |> put_layout(false)
215 | |> put_view(InstaghubWeb.HtmlView)
216 | |> render("posts.html", posts: feeds_with_page.edge_hashtag_to_media.posts, page_info: feeds_with_page.edge_hashtag_to_media.page_info)
217 | end
218 | end
219 |
220 | def search(conn, %{"item" => item} = _params) do
221 | cursor = Cache.get_cursor(conn)
222 | redis_key_md5 = Cache.get_page_key(conn, cursor)
223 | page = Cache.get_page(conn)
224 | feeds_with_page =
225 | if page == nil do
226 | feeds_with_page = API.search_tags_users(item)
227 | if feeds_with_page != nil do
228 | feeds_bin = :erlang.term_to_binary(feeds_with_page)
229 | RedisUtil.setx(redis_key_md5, feeds_bin)
230 | Logger.debug "get page with api and store in redis with key #{redis_key_md5}"
231 | end
232 | feeds_with_page
233 | else
234 | page
235 | end
236 | if is_4xx(feeds_with_page) do
237 | handle_4xx(conn, feeds_with_page)
238 | else
239 | tags = feeds_with_page.hashtags
240 | users = feeds_with_page.users
241 | render(conn, "search.html", items: tags ++ users |> Enum.shuffle)
242 | end
243 | end
244 |
245 | def privacy(conn, _params) do
246 | render(conn, "privacy.html", [])
247 | end
248 |
249 | def about(conn, _params) do
250 | render(conn, "about.html", [])
251 | end
252 | end
253 |
--------------------------------------------------------------------------------
/lib/instaghub_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :instaghub
3 |
4 | socket "/socket", InstaghubWeb.UserSocket,
5 | websocket: true,
6 | longpoll: false
7 |
8 | # Serve at "/" the static files from "priv/static" directory.
9 | #
10 | # You should set gzip to true if you are running phx.digest
11 | # when deploying your static files in production.
12 | plug Plug.Static,
13 | at: "/",
14 | from: :instaghub,
15 | gzip: false,
16 | only: ~w(css fonts images js favicon.ico robots.txt)
17 |
18 | # Code reloading can be explicitly enabled under the
19 | # :code_reloader configuration of your endpoint.
20 | if code_reloading? do
21 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
22 | plug Phoenix.LiveReloader
23 | plug Phoenix.CodeReloader
24 | end
25 |
26 | plug Plug.RequestId
27 | plug Plug.Logger
28 |
29 | plug Plug.Parsers,
30 | parsers: [:urlencoded, :multipart, :json],
31 | pass: ["*/*"],
32 | json_decoder: Phoenix.json_library()
33 |
34 | plug Plug.MethodOverride
35 | plug Plug.Head
36 |
37 | # The session will be stored in the cookie and signed,
38 | # this means its contents can be read but not tampered with.
39 | # Set :encryption_salt if you would also like to encrypt it.
40 | plug Plug.Session,
41 | store: :cookie,
42 | key: "_instaghub_key",
43 | signing_salt: "b+OgsM2h"
44 |
45 | plug InstaghubWeb.Router
46 | end
47 |
--------------------------------------------------------------------------------
/lib/instaghub_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import InstaghubWeb.Gettext
9 |
10 | # Simple translation
11 | gettext("Here is the string to translate")
12 |
13 | # Plural translation
14 | ngettext("Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3)
17 |
18 | # Domain-based translation
19 | dgettext("errors", "Here is the error message to translate")
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :instaghub
24 | end
25 |
--------------------------------------------------------------------------------
/lib/instaghub_web/plugs/cache.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb.Plug.Cache do
2 | alias Instaghub.RedisUtil
3 | alias Instaghub.Utils
4 | require Logger
5 |
6 | @page_cursor "cursor"
7 | @page_key :page
8 |
9 | def init(options) do
10 | # initialize options
11 | options
12 | end
13 |
14 | def call(conn, _params) do
15 | cursor = get_cursor(conn)
16 | redis_key_md5 = get_page_key(conn, cursor)
17 | value = RedisUtil.get(redis_key_md5)
18 | feeds_with_page =
19 | if value == nil do
20 | nil
21 | else
22 | Logger.debug "get page with redis key #{redis_key_md5}"
23 | :erlang.binary_to_term value
24 | end
25 | Map.put(conn, @page_key, feeds_with_page)
26 | end
27 |
28 | def get_page(conn) do
29 | Map.get(conn, @page_key)
30 | end
31 |
32 | def get_cursor(%Plug.Conn{params: params}) do
33 | Map.get(params, @page_cursor)
34 | end
35 |
36 | def get_page_key(%Plug.Conn{request_path: path}, cursor) do
37 | redis_key = if cursor == nil do
38 | path
39 | else
40 | Logger.debug "request_path #{path}"
41 | path <> cursor
42 | end
43 | Utils.md5_base64(redis_key)
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/instaghub_web/plugs/qps.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb.Plug.QPS do
2 | require Logger
3 | alias Instaghub.Utils
4 |
5 | @max_qps_googlebot 1
6 | @max_qps_human 5
7 |
8 | def init(options) do
9 | # initialize options
10 | options
11 | end
12 |
13 | def call(conn, _params) do
14 | ua_type = conn
15 | |> Utils.check_ua_type
16 | Instaghub.Bucket.increase_req(ua_type)
17 | if is_need_check_qps(conn.request_path) do
18 | conn |> check_qps(ua_type)
19 | else
20 | conn
21 | end
22 | end
23 |
24 | defp is_need_check_qps(req_path) do
25 | if String.contains?(req_path, "/tag/") || String.contains?(req_path, "/user/") do
26 | true
27 | else
28 | false
29 | end
30 | end
31 |
32 | def check_qps(conn, ua_type) do
33 | case ua_type do
34 | :googlebot -> googlebot_action(conn, ua_type)
35 | :otherbot -> otherbot_action(conn, ua_type)
36 | _ -> human_action(conn, ua_type)
37 | end
38 | end
39 |
40 | defp googlebot_action(conn, ua_type) do
41 | qps = Instaghub.Bucket.get_req(ua_type)
42 | Logger.info "req is google bot, current req count is #{qps}"
43 | ins_google_qps = System.get_env("INS_GOOGLE_QPS")
44 | google_qps = if ins_google_qps != nil do
45 | ins_google_qps |> String.to_integer
46 | else
47 | @max_qps_googlebot
48 | end
49 | if qps > google_qps do
50 | Logger.info "beyond max googlebot qps(#{google_qps}) and we will halt all request"
51 | conn
52 | |> Plug.Conn.put_status(:too_many_requests)
53 | |> Phoenix.Controller.put_view(InstaghubWeb.ErrorView)
54 | |> Phoenix.Controller.render("429.html", %{})
55 | |> before_halt(ua_type)
56 | |> Plug.Conn.halt
57 | else
58 | conn
59 | end
60 | end
61 |
62 | defp otherbot_action(conn, ua_type) do
63 | Logger.info "if other bot we will halt all request"
64 | conn
65 | |> Plug.Conn.put_status(:too_many_requests)
66 | |> Phoenix.Controller.put_view(InstaghubWeb.ErrorView)
67 | |> Phoenix.Controller.render("429.html", %{})
68 | |> before_halt(ua_type)
69 | |> Plug.Conn.halt
70 | end
71 |
72 | defp human_action(conn, ua_type) do
73 | qps = Instaghub.Bucket.get_req(ua_type)
74 | Logger.info "req is human, current req count is #{qps}"
75 | if qps > @max_qps_human do
76 | Logger.info "beyond max human qps and we will redirect to index"
77 | conn
78 | |> Phoenix.Controller.redirect(to: "/")
79 | #|> before_halt(ua_type)
80 | |> Plug.Conn.halt
81 | else
82 | conn
83 | end
84 | end
85 |
86 | defp before_halt(conn, ua_type) do
87 | Logger.debug "before halt we will decrease req #{ua_type}"
88 | Instaghub.Bucket.decrease_req(ua_type)
89 | conn
90 | end
91 |
92 | end
93 |
--------------------------------------------------------------------------------
/lib/instaghub_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb.Router do
2 | use InstaghubWeb, :router
3 | alias InstaghubWeb.Plug.Cache
4 | alias InstaghubWeb.Plug.QPS
5 | alias Instaghub.Utils
6 | require Logger
7 |
8 | pipeline :browser do
9 | plug :accepts, ["html"]
10 | plug :fetch_session
11 | plug :fetch_flash
12 | plug :protect_from_forgery
13 | plug :put_secure_browser_headers
14 | plug :redirect_to_index
15 | plug Cache, []
16 | # plug QPS
17 | # plug :dec_qps
18 | end
19 |
20 | def redirect_to_index(%Plug.Conn{request_path: "/" <> path} = conn, _opts) when path != "" do
21 | conn
22 | |> Plug.Conn.put_status(:moved_permanently)
23 | |> Phoenix.Controller.redirect(to: "/")
24 | |> Plug.Conn.halt
25 | end
26 |
27 | def redirect_to_index(%Plug.Conn{request_path: "/"} = conn, _opts) do
28 | conn
29 | end
30 |
31 | def dec_qps(conn, _opts) do
32 | Plug.Conn.register_before_send(conn, fn conn ->
33 | Logger.debug "start decrease req in plug"
34 | conn
35 | |> Utils.check_ua_type
36 | |> Instaghub.Bucket.decrease_req
37 | conn
38 | end)
39 | end
40 |
41 | pipeline :api do
42 | plug :accepts, ["json"]
43 | end
44 |
45 | scope "/", InstaghubWeb do
46 | pipe_through :browser
47 |
48 | get "/", PageController, :index
49 | get "/explore/women", PageController, :index
50 | get "/explore/animal", PageController, :index
51 | get "/explore/game", PageController, :index
52 | get "/explore/food", PageController, :index
53 | get "/explore/hot", PageController, :index
54 | get "/post/:shortcode", PageController, :post_comment
55 | get "/user/:username", PageController, :user_posts
56 | get "/tag/:tagname", PageController, :tag_posts
57 | get "/search/:item", PageController, :search
58 | get "/privacy", PageController, :privacy
59 | get "/about", PageController, :about
60 | get "/*path", PageController, :not_found
61 | end
62 |
63 | # Other scopes may use custom stacks.
64 | # scope "/api", InstaghubWeb do
65 | # pipe_through :api
66 | # end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/instaghub_web/seo.ex:
--------------------------------------------------------------------------------
1 | defmodule InstaghubWeb.Model.TKD do
2 | defstruct title: nil, keywords: nil, description: nil
3 | end
4 |
5 | defmodule InstaghubWeb.SEO do
6 | def get_index_seo(path, page) do
7 | case path do
8 | "/" -> get_sports_seo(page)
9 | "/explore/women" -> get_women_seo(page)
10 | "/explore/women/" -> get_women_seo(page)
11 | "/explore/animal" -> get_animal_seo(page)
12 | "/explore/animal/" -> get_animal_seo(page)
13 | "/explore/game" -> get_game_seo(page)
14 | "/explore/game/" -> get_game_seo(page)
15 | "/explore/food" -> get_food_seo(page)
16 | "/explore/food/" -> get_food_seo(page)
17 | "/explore/hot" -> get_hot_seo(page)
18 | "/explore/hot/" -> get_hot_seo(page)
19 | _ -> get_sports_seo(page)
20 | end
21 | end
22 |
23 | defp get_sports_seo(_page) do
24 | title = "Instaghub.com - Your instagram viewer no need login!"
25 | description = "Find hot users, hashtags, posts about women, animals, games, foods on instagram. Instaghub.com is your instagram viewer no need login!"
26 | keywords = "NBA, LeBron James, NFL, 9GAG, House of Highlights, Juventus Football Club, FC Barcelona, Leo Messi, Cristiano Ronaldo, LaLiga, UEFA Champions League, SportsCenter, espn, Los Angeles Lakers"
27 | %{title: title, description: description, keywords: keywords}
28 | end
29 |
30 | defp get_women_seo(_page) do
31 | title = "Instaghub.com - Your instagram viewer no need login!"
32 | description = "Find hot users, hashtags, posts about women, animals, games, foods on instagram. Instaghub.com is your instagram viewer no need login!"
33 | keywords = "Women swear, women clothing, women fashion, women lift, fashion designer, women explore, women health, women shoes, women fitness, women fitness"
34 | %{title: title, description: description, keywords: keywords}
35 | end
36 |
37 | defp get_animal_seo(_page) do
38 | title = "Instaghub.com - Your instagram viewer no need login!"
39 | description = "Find hot users, hashtags, posts about women, animals, games, foods on instagram. Instaghub.com is your instagram viewer no need login!"
40 | keywords = "Lovely Cats, kitten, kitty, dogs, lovely dogs, dogs showtimes, dogs adventures, dog training, puppy"
41 | %{title: title, description: description, keywords: keywords}
42 | end
43 |
44 | defp get_game_seo(_page) do
45 | title = "Instaghub.com - Your instagram viewer no need login!"
46 | description = "Find hot users, hashtags, posts about women, animals, games, foods on instagram. Instaghub.com is your instagram viewer no need login!"
47 | keywords = "car,Maserati,Jaguar,Mercedes-Benz,Mercedes-AMG,Porsche,BMW,Ferrari,BUGATTI,Lamborghini,Cosplay, cosplayer, game video, nba2k, minecraft, playstation, blizzard, pokemon, nintendo, fortnite, twitch, fazeclan, pubg"
48 | %{title: title, description: description, keywords: keywords}
49 | end
50 |
51 | defp get_food_seo(_page) do
52 | title = "Instaghub.com - Your instagram viewer no need login!"
53 | description = "Find hot users, hashtags, posts about women, animals, games, foods on instagram. Instaghub.com is your instagram viewer no login!"
54 | keywords = "Food cook, food discoover, icecream, kitchen, food craft, food video, healthy food, buzzfeed food, chocolate, dinner"
55 | %{title: title, description: description, keywords: keywords}
56 | end
57 |
58 | defp get_hot_seo(_page) do
59 | title = "Instaghub.com - Your instagram viewer no need login!"
60 | description = "Find hot users, hashtags, posts about women, animals, games, foods on instagram. Instaghub.com is your instagram viewer no need login!"
61 | keywords = "Gameofthrones, peterdinklage,hbo,imdb,hot tv show, marvel, therock, thehughjackman, leonardodicaprio, hot firms, hot tv videos, Warner Bros, Sony Pictures, Walt Disney, Universal Pictures, 20th Century Fox, The Weinstein Company, DreamWorks Pictures"
62 | %{title: title, description: description, keywords: keywords}
63 | end
64 |
65 | def get_user_seo(page) do
66 | try do
67 | title = page.username <> "'s instagram photos and videos - Instaghub.com"
68 | description = page.biography
69 | keywords = page.username <> ", following " <> "#{page.edge_follow.count}" <> ", followed by " <> "#{page.edge_followed_by.count}"
70 | %{title: title, description: description, keywords: keywords}
71 | rescue
72 | _ -> nil
73 | end
74 | end
75 |
76 | def get_tag_seo(page) do
77 | try do
78 | title = page.name <> "'s instagram photos and videos - Instaghub.com"
79 | description = "watch " <> page.name <> " " <> "#{page.edge_hashtag_to_media.count}" <> " photos and videos"
80 | keywords = page.name
81 | %{title: title, description: description, keywords: keywords}
82 | rescue
83 | _ -> nil
84 | end
85 | end
86 |
87 | def get_post_seo(page) do
88 | try do
89 | title = page.edge_media_to_caption <> " by " <> page.owner.username <> " - Instaghub.com"
90 | description = page.edge_media_to_caption
91 | keywords = page.owner.username
92 | %{title: title, description: description, keywords: keywords}
93 | rescue
94 | _ -> nil
95 | end
96 | end
97 | end
98 |
--------------------------------------------------------------------------------
/lib/instaghub_web/templates/error/404.html:
--------------------------------------------------------------------------------
1 |
404
2 | -------------------------------------------------------------------------------- /lib/instaghub_web/templates/error/429.html: -------------------------------------------------------------------------------- 1 |There is too many requests, try later.
2 | -------------------------------------------------------------------------------- /lib/instaghub_web/templates/html/posts.html.eex: -------------------------------------------------------------------------------- 1 | 4 | 7 | <%= for {post, ins} <- Enum.with_index(@posts) do %> 8 |<%= Instaghub.Utils.parse_link(post.edge_media_to_caption) %>
26 |<%= post.taken_at_timestamp %>
29 |Instaghub is an advanced Instagram marketing platform, aiming at helping brands, advertisers, and publishers find and explore the latest trends of hashtags, users, photos and videos on Instagram.
4 |<%= Instaghub.Utils.parse_link(post.edge_media_to_caption) %>
22 |<%= post.taken_at_timestamp %>
25 |@<%= @post.owner.username %>
33 | 34 | <%= @post.taken_at_timestamp %> 35 |We recognize that your privacy is very important and take it seriously, which is why we developed this privacy policy to explain how we collect, use, communicate, disclose and make use of personal information.
4 |This site uses the Instagram API but is not endorsed or certified by Instagram. All Instagram™ logos and trademarks displayed on this applicatioin are property of Instagram.
5 |Instaghub uses Instagram's data and APIs, please checkout Instagram's Terms of Use and Privacy Policy. Instaghub is dependent on Instagram's API and will work as long as Instagram service is available.
6 |Our site is not directed to children under 13. If you become aware that your child has provided Us with personal information without your consent, please contact us at sys(at)i365.tech. We do not knowingly collect personal information from children under 13. If We become aware that a child under 13 has provided Us with personal information, We take steps to remove such information.
7 |Our site may contain links to third party websites which may or may not have affiliation with. These third party sites have separate and independent privacy policies. We therefore have no responsibility or liability for the content and activities of these linked sites. We do not share your personal information with those third parties except as discussed in this document.
8 |Cookies are small files that Instaghub transfers to your computer's hard drive through your web browser (if you allow this) that enables Instaghub to recognize your browser and capture and remember certain information. Our cookies are linked to your login information so that you can use Instaghub from your computer throughout the day without logging in each time you visit us.
9 |If you have any questions or concerns about this Privacy Policy or its implementation you may contact us at sys(at)i365.tech.
10 |@<%= item.username %>
10 | 11 |#<%= item.name %>
17 | 18 |posts: <%= item.media_count %>
19 |#<%= @tag.name %>
10 |posts: <%= @tag.edge_hashtag_to_media.count %>
11 |<%= Instaghub.Utils.parse_link(post.edge_media_to_caption) %>
36 |<%= post.taken_at_timestamp %>
39 |<%= Instaghub.Utils.parse_link(post.edge_media_to_caption) %>
41 |<%= post.taken_at_timestamp %>
44 |
<%= Instaghub.Utils.parse_link(comment.text) %> - <%= comment.created_at %>
73 |