├── .gitignore
├── .vscode
└── launch.json
├── README.md
├── arconnect_notes
├── diagrams
└── permacast.png
├── package.json
├── podcasts
├── new.js
├── podcast.js
├── podcast.json
└── podcastNew.js
├── public
├── alt-favicon.ico
├── favicon.ico
├── index.html
├── locales
│ ├── en
│ │ └── translation.json
│ ├── uk
│ │ └── translation.json
│ └── zh
│ │ └── translation.json
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── src
├── App.js
├── App.test.js
├── component
│ ├── arconnect_loader.jsx
│ ├── index.jsx
│ ├── navbar.jsx
│ ├── podcast.jsx
│ ├── podcast_html.jsx
│ ├── podcast_rss.jsx
│ ├── podcast_utils.jsx
│ ├── upload_episode.jsx
│ ├── upload_show.jsx
│ └── wallet_loader.jsx
├── i18n.js
├── index.js
├── logo.svg
├── reportWebVitals.js
├── setupTests.js
├── utils
│ ├── arweave.js
│ ├── initStateGen.js
│ ├── podcast.js
│ ├── shorthands.js
│ └── theme.js
└── yellow-rec.svg
├── tailwind.config.js
├── v2-contracts
├── v2.js
└── v2.json
├── v3
├── oracle
│ ├── oracle.js
│ └── oracle.json
├── v3.js
└── v3.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | # tailwindcss
22 | src/tailwind.css
23 |
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "pwa-chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome against localhost",
11 | "url": "http://localhost:8080",
12 | "webRoot": "${workspaceFolder}"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Permacast
2 |
3 | Permacast is a podcasting hosting and discovery platform powered by the Arweave Permaweb. Let you podcasts live forever with censorship resistance.
4 |
5 | ## Workflow
6 |
7 | Any content creator (podcasts) can use the permacast platform to perma-host his/her podcasts and its episodes. The audio files are permanently archived in Arweave's Permaweb once uploaded to Permacast frontend.
8 |
9 | Each content-creator needs an Arweave wallet topped-up with AR tokens to deploy a contract, and start adding episodes.
10 |
11 |
12 |
13 | ## Permacast Smart Contracts
14 | | Name | Path | Onchain Source Code |
15 | | ------------- |:-------------:| ------------- |
16 | | Permacast V2 Master Contract Src | [./v2-contracts](./v2-contracts) | [KrMNSCljeT0sox8bengHf0Z8dxyE0vCTLEAOtkdrfjM](https://viewblock.io/arweave/tx/KrMNSCljeT0sox8bengHf0Z8dxyE0vCTLEAOtkdrfjM) |
17 | | Permacast V3 Master Contract Src | [./v3](./v3) | [-SoIrUzyGEBklizLQo1w5AnS7uuOB87zUrg-kN1QWw4](https://viewblock.io/arweave/tx/-SoIrUzyGEBklizLQo1w5AnS7uuOB87zUrg-kN1QWw4) |
18 | | Factories Oracle Contract Address | [./v3/oracle](./v3/oracle) | [8K77MdQ855XCjdbwAO-SjeB89z3tlWGQYDowAHR45pA](https://viewblock.io/arweave/address/8K77MdQ855XCjdbwAO-SjeB89z3tlWGQYDowAHR45pA) |
19 |
20 | ## Front-Ends Access:
21 | - Production version -- Decentralized UI hosting: [permacast.net](https://permacast.net)
22 | - Development version -- Centralized UI hosting: [permacast.dev](https://permacast.dev)
23 |
24 | ## Tech-Stack
25 | - Frontend: React
26 | - Backend: SmartWeave contracts
27 | - Gateway: [Meson Network](https://meson.network/)
28 | - UI Hosting: [Spheron Network](https://spheron.network/)
29 |
30 | ## Permacast API
31 | [permacast-cache](https://github.com/Parallel-news/permacast-cache) is the repositiry of the API of Permacast FE.
32 |
33 | ## Documentation & Guides
34 | You can find guide and tutorials on how to use Permacast [here](https://github.com/Parallel-news/permacast-docs).
35 |
36 | ## License
37 |
38 | Permacast is licensed under the [MIT license](./LICENSE).
39 |
40 |
--------------------------------------------------------------------------------
/arconnect_notes:
--------------------------------------------------------------------------------
1 | sessionStorage ->
2 | wallet_address
3 | arweaveWallet
4 |
5 |
6 |
--------------------------------------------------------------------------------
/diagrams/permacast.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Parallel-news/permacast/384bc1fcd91d4e1e3468e7afe807e35fab8d84ec/diagrams/permacast.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "permacast",
3 | "version": "0.3.2",
4 | "private": true,
5 |
6 | "dependencies": {
7 | "@headlessui/react": "^1.5.0",
8 | "@heroicons/react": "^1.0.6",
9 | "@tailwindcss/aspect-ratio": "^0.4.0",
10 | "@tailwindcss/typography": "^0.5.1",
11 | "@testing-library/jest-dom": "^5.11.4",
12 | "@testing-library/react": "^12.1.2",
13 | "@testing-library/user-event": "^13.5.0",
14 | "ardb": "^1.1.9",
15 | "arweave": "^1.10.22",
16 | "arweave-fees.js": "^0.0.2",
17 | "arweave-multihost": "^0.1.0",
18 | "autoprefixer": "^10.4.2",
19 | "concurrently": "^7.0.0",
20 | "daisyui": "^2.6.0",
21 | "i18next": "^21.6.13",
22 | "i18next-browser-languagedetector": "^6.1.3",
23 | "i18next-http-backend": "^1.3.2",
24 | "postcss": "^8.4.6",
25 | "react": "^17.0.2",
26 | "react-dom": "^17.0.2",
27 | "react-dropzone": "^11.3.4",
28 | "react-i18next": "^11.15.5",
29 | "react-icons": "^4.2.0",
30 | "react-router": "^5.2.0",
31 | "react-router-dom": "^5.2.0",
32 | "react-scripts": "4.0.3",
33 | "shikwasa": "^2.1.2",
34 | "smartweave": "^0.4.41",
35 | "sweetalert2": "^11.4.0",
36 | "tailwindcss": "^3.0.22",
37 | "theme-change": "^2.0.2",
38 | "web-vitals": "^2.1.4"
39 | },
40 | "devDependencies": {
41 | "react-error-overlay": "6.0.9"
42 | },
43 | "scripts": {
44 | "start": "concurrently \"npm run start:css\" \"react-scripts start\"",
45 | "start:css": "tailwindcss -o src/tailwind.css --watch",
46 | "build": "npm run build:css && react-scripts build",
47 | "build:css": "NODE_ENV=production tailwindcss -o src/tailwind.css -m",
48 | "test": "react-scripts test",
49 | "eject": "react-scripts eject"
50 | },
51 | "eslintConfig": {
52 | "extends": ["react-app", "react-app/jest"]
53 | },
54 | "browserslist": {
55 | "production": [">0.2%", "not dead", "not op_mini all"],
56 | "development": [
57 | "last 1 chrome version",
58 | "last 1 firefox version",
59 | "last 1 safari version"
60 | ]
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/podcasts/new.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SWC used as first level data registery for
3 | * Arweave hosted podcasts.
4 | *
5 | * The current contract represents a basic PoC
6 | *
7 | * contributor(s): charmful0x
8 | *
9 | * Lisence: MIT
10 | **/
11 |
12 |
13 |
14 | export async function handle(state, action) {
15 | const input = action.input
16 | const caller = action.caller
17 | const podcasts = state.podcasts
18 |
19 | const contractID = SmartWeave.contract.id
20 | const contractTxObject = await SmartWeave.unsafeClient.transactions.get(contractID)
21 | const base64Owner = contractTxObject["owner"]
22 | const contractOwner = await SmartWeave.unsafeClient.wallets.ownerToAddress(base64Owner)
23 |
24 | if (input.function === "createPodcast") {
25 | const name = input.name
26 | const desc = input.desc
27 | const cover = input.cover
28 |
29 | const pid = SmartWeave.transaction.id
30 | const tagsMap = new Map();
31 |
32 | if (caller !== contractOwner ) {
33 | throw new ContractError('invalid caller. Only the contractOwner can perform this action')
34 | }
35 |
36 | if (typeof name !== "string" || name.length > 50) {
37 | throw new ContractError('uncorrect name limit')
38 | }
39 |
40 | if (typeof desc !== "string" || desc.length > 500) {
41 | throw new ContractError('description too long')
42 | }
43 |
44 | // validate the cover TXID. it should be an Arweave data
45 | // TX having image/x mime type
46 |
47 | // <------------------------
48 | if (typeof cover !== "string" || cover.length !== 43) {
49 | throw new ContractError('uncorrect cover format')
50 | }
51 |
52 | const coverTx = await SmartWeave.unsafeClient.transactions.get(cover)
53 | const tags = coverTx.get("tags")
54 |
55 | for (let tag of tags) {
56 | const key = tag.get("name", {decode: true, string: true} )
57 | const value = tag.get("value", {decode: true, string: true})
58 | tagsMap.set(key, value)
59 | }
60 |
61 | if (! tagsMap.has("Content-Type")) {
62 | throw new ContractError('uncorrect data transaction')
63 | }
64 |
65 | if (! tagsMap.get("Content-Type").startsWith("image/") ) {
66 | throw new ContractError('invalid mime type')
67 | }
68 |
69 | // ------------------------>
70 |
71 | podcasts.push({
72 | "pid": pid,
73 | "index": _getPodcastIndex(), // id equals the index of the podacast obj in the podcasts array
74 | "owner": caller,
75 | "podcastName": name,
76 | "description": desc,
77 | "cover": cover,
78 | "episodes":[],
79 | "logs": [pid]
80 | })
81 |
82 | return { state }
83 | }
84 |
85 | if ( input.function === "addEpisode") {
86 | const index = input.index // podcasts index
87 | const name = input.name
88 | const audio = input.audio // the TXID of 'audio/' data
89 | const desc = input.desc
90 |
91 | const tagsMap = new Map()
92 |
93 | if (caller !== contractOwner ) {
94 |
95 | throw new ContractError('invalid caller. Only the contractOwner can perform this action')
96 | }
97 |
98 | if (! podcasts[index]) {
99 | throw new ContractError('podcast the given having index not found')
100 | }
101 |
102 | if (typeof name !== "string" || name.length > 50) {
103 | throw new ContractError('uncorrect name limit')
104 | }
105 |
106 | if (typeof desc !== "string" || desc.length > 250) {
107 | throw new ContractError('description too long')
108 | }
109 |
110 | if (typeof audio !== "string" || audio.length !== 43) {
111 | throw new ContractError('invalid audio TX type')
112 | }
113 |
114 | const audioTx = await SmartWeave.unsafeClient.transactions.get(audio)
115 | const tags = audioTx.get("tags")
116 |
117 | for (let tag of tags) {
118 | const key = tag.get("name", {decode: true, string: true} )
119 | const value = tag.get("value", {decode: true, string: true})
120 | tagsMap.set(key, value)
121 | }
122 |
123 | if (! tagsMap.has("Content-Type")) {
124 | throw new ContractError('uncorrect data transaction')
125 | }
126 |
127 | if (! tagsMap.get("Content-Type").startsWith("audio/") ) {
128 | throw new ContractError('invalid mime type')
129 | }
130 |
131 | podcasts[index]["episodes"].push({
132 | "eid": SmartWeave.transaction.id,
133 | "childOf": index,
134 | "episodeName": name,
135 | "description": desc,
136 | "audioTx": audio,
137 | "uploadedAt": SmartWeave.block.height,
138 | "logs": [SmartWeave.transaction.id]
139 | })
140 |
141 | return { state }
142 |
143 | }
144 |
145 |
146 |
147 | // PODCAST ACTIONS:
148 |
149 | if (input.function === "deletePodcast") {
150 | const index = input.index
151 |
152 | if ( caller !== contractOwner) {
153 | throw new ContractError('invalid caller. Only the contractOwner can perform this action')
154 | }
155 |
156 | if (! Number.isInteger(index) ) {
157 | throw new ContractError('invalid index')
158 | }
159 |
160 | if (! podcasts[index]) {
161 | throw new ContractError('podcast having the gievn index does not exist')
162 | }
163 |
164 | podcasts.splice(index, 1)
165 |
166 | return { state }
167 | }
168 |
169 | if (input.function === "editPodcastName") {
170 | const index = input.index
171 | const name = input.name
172 |
173 | const actionTx = SmartWeave.transaction.id
174 |
175 | if ( caller !== contractOwner) {
176 | throw new ContractError('invalid caller. Only the contractOwner can perform this action')
177 | }
178 |
179 | if (! Number.isInteger(index) ) {
180 | throw new ContractError('invalid index')
181 | }
182 |
183 | if (! podcasts[index]) {
184 | throw new ContractError('podcast having the given index does not exist')
185 | }
186 |
187 | if (typeof name !== "string") {
188 | throw new ContractError('invalid name type')
189 | }
190 |
191 | if ( name.length < 3 || name.length > 50 ) {
192 | throw new ContractError('the name does not meet the name limits')
193 | }
194 |
195 | if ( podcasts[index]["podcastName"] === name) {
196 | throw new ContractError('old name and new name cannot be equals')
197 | }
198 |
199 | podcasts[index]["podcastName"] = name
200 | podcasts[index]["logs"].push(actionTx)
201 |
202 | return { state }
203 | }
204 |
205 | if (input.function === "editPodcastDesc") {
206 | const index = input.index
207 | const desc = input.desc
208 |
209 | const actionTx = SmartWeave.transaction.id
210 |
211 | if ( caller !== contractOwner) {
212 | throw new ContractError('invalid caller. Only the contractOwner can perform this action')
213 | }
214 |
215 | if (! Number.isInteger(index) ) {
216 | throw new ContractError('invalid index')
217 | }
218 |
219 | if (! podcasts[index]) {
220 | throw new ContractError('podcast having the given ID does not exist')
221 | }
222 |
223 | if ( typeof desc !== "string" ) {
224 | throw new ContractError('invalid description type')
225 | }
226 |
227 | if ( desc.length > 250 ) {
228 | throw new ContractError('description length too high')
229 | }
230 |
231 | if ( podcasts[index]["description"] === desc ) {
232 | throw new ContractError('old description and new description cannot be equals')
233 | }
234 |
235 | podcasts[index]["description"] = desc
236 | podcasts[index]["logs"].push(actionTx)
237 |
238 | return { state }
239 |
240 | }
241 |
242 | if (input.function === "editPodcastCover") {
243 | const index = input.index
244 | const cover = input.cover
245 | const actionTx = SmartWeave.transaction.id
246 | const tagsMap = new Map();
247 |
248 | if ( caller !== contractOwner) {
249 | throw new ContractError('invalid caller. Only the contractOwner can perform this action')
250 | }
251 |
252 | if (! Number.isInteger(index) ) {
253 | throw new ContractError('invalid index')
254 | }
255 |
256 | if (! podcasts[index]) {
257 | throw new ContractError('podcast having the given id does not exist')
258 | }
259 |
260 | if (typeof cover !== "string" || cover.length !== 43) {
261 | throw new ContractError('uncorrect cover format')
262 | }
263 |
264 | const coverTx = await SmartWeave.unsafeClient.transactions.get(cover)
265 | const tags = coverTx.get("tags")
266 |
267 | for (let tag of tags) {
268 | const key = tag.get("name", {decode: true, string: true} )
269 | const value = tag.get("value", {decode: true, string: true})
270 | tagsMap.set(key, value)
271 | }
272 |
273 | if (! tagsMap.has("Content-Type")) {
274 | throw new ContractError('uncorrect data transaction')
275 | }
276 |
277 | if (! tagsMap.get("Content-Type").startsWith("image/") ) {
278 | throw new ContractError('invalid mime type')
279 | }
280 |
281 | if ( podcasts[index]["cover"] === cover ) {
282 | throw new ContractError('old cover and new cover cannot be equals')
283 | }
284 |
285 | podcasts[index]["cover"] = cover
286 | podcasts[index]["logs"].push(actionTx)
287 |
288 | return { state }
289 |
290 | }
291 |
292 |
293 |
294 |
295 | // EPISODES ACTIONS:
296 |
297 | if (input.function === "editEpisodeName") {
298 | const name = input.name
299 | const index = input.index //podcast index
300 | const id = input.id // episode's index
301 |
302 | const actionTx = SmartWeave.transaction.id
303 |
304 | if (caller !== contractOwner) {
305 | throw new ContractError('invalid caller. Only the contractOwner} can perform this action')
306 | }
307 |
308 | if (! podcasts[index]) {
309 | throw new ContractError('podcast having the given ID not found')
310 | }
311 |
312 | if (! podcasts[index]["episodes"][id]) {
313 | throw new ContractError('episode having the given index not found')
314 | }
315 |
316 | if (typeof name !== "string") {
317 | throw new ContractError('invalid name format')
318 | }
319 |
320 | if (name.length < 2 || name.length > 50) {
321 | throw new ContractError('name does not meet the name limits')
322 | }
323 |
324 | if ( podcasts[index]["episodes"][id]["episodeName"] === name ) {
325 | throw new ContractError('new name and old name cannot be the same')
326 | }
327 |
328 | podcasts[index]["episodes"][id]["episodeName"] = name
329 | podcasts[index]["episodes"][id]["logs"].push(actionTx)
330 |
331 |
332 | return { state }
333 | }
334 |
335 | if (input.function === "editEpisodeDesc") {
336 | const index = input.index
337 | const id = input.id
338 | const desc = input.desc
339 |
340 | const actionTx = SmartWeave.transaction.id
341 |
342 | if (caller !== contractOwner) {
343 | throw new ContractError('invalid caller. Only the contractOwner can perform this action')
344 | }
345 |
346 | if (! podcasts[index]) {
347 | throw new ContractError('podcast having the given ID not found')
348 | }
349 |
350 | if (! podcasts[index]["episodes"][id]) {
351 | throw new ContractError('episode having the given index id not found')
352 | }
353 |
354 | if (typeof desc !== "string") {
355 | throw new ContractError('invalid description format')
356 | }
357 |
358 | if ( desc.length < 25 || desc.length > 500 ) {
359 | throw new ContractError('the description text does not meet the desc limits')
360 | }
361 |
362 | if ( podcasts[index]["episodes"][id]["description"] === desc) {
363 | throw new ContractError('old description and new description canot be the same')
364 | }
365 |
366 | podcasts[index]["episodes"][id]["description"] = desc
367 | podcasts[index]["episodes"][id]["logs"].push(actionTx)
368 |
369 | return { state }
370 | }
371 |
372 | if (input.function === "deleteEpisode") {
373 | const index = input.index
374 | const id = input.id
375 |
376 | if ( caller !== contractOwner) {
377 | throw new ContractError('invalid caller. Only the contractOwner can perform this action')
378 | }
379 |
380 | if (! podcasts[index]) {
381 | throw new ContractError('podcast having the given ID not found')
382 | }
383 |
384 | if (! podcasts[index]["episodes"][id]) {
385 | throw new ContractError('episode having the given index not found')
386 | }
387 |
388 | podcasts[index]["episodes"].splice(id, 1)
389 |
390 | return { state }
391 | }
392 |
393 | // HELPER FUNCTIONS:
394 | function _getPodcastIndex() {
395 | if (podcasts.length === 0) {
396 | return 0
397 | }
398 |
399 | return (podcasts.length - 1 )
400 | }
401 |
402 | throw new ContractError('unknow function supplied')
403 | }
404 |
--------------------------------------------------------------------------------
/podcasts/podcast.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SWC used as first level data registery for
3 | * Arweave hosted podcasts.
4 | *
5 | * The current contract represents a basic PoC
6 | *
7 | * contributor(s): charmful0x
8 | *
9 | * Lisence: MIT
10 | **/
11 |
12 |
13 |
14 | export async function handle(state, action) {
15 | const input = action.input
16 | const caller = action.caller
17 | const podcasts = state.podcasts
18 |
19 | const contractID = SmartWeave.contract.id
20 | const contractTxObject = await SmartWeave.unsafeClient.transactions.get(contractID)
21 | const base64Owner = contractTxObject["owner"]
22 | const contractOwner = await SmartWeave.unsafeClient.wallets.ownerToAddress(base64Owner)
23 |
24 | if (input.function === "createPodcast") {
25 | const name = input.name
26 | const desc = input.desc
27 | const cover = input.cover
28 | const pid = SmartWeave.transaction.id
29 |
30 | const tagsMap = new Map();
31 |
32 | if (caller !== contractOwner ) {
33 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
34 | }
35 |
36 | if (typeof name !== "string" || name.length > 50) {
37 | throw new ContractError(`uncorrect name limit`)
38 | }
39 |
40 | if (typeof desc !== "string" || desc.length > 500) {
41 | throw new ContractError(`description too long`)
42 | }
43 |
44 | // validate the cover TXID. it should be an Arweave data
45 | // TX having image/x mime type
46 |
47 | // <------------------------
48 | if (typeof cover !== "string" || cover.length !== 43) {
49 | throw new ContractError(`uncorrect cover format`)
50 | }
51 |
52 | const coverTx = await SmartWeave.unsafeClient.transactions.get(cover)
53 | const tags = coverTx.get("tags")
54 |
55 | for (let tag of tags) {
56 | const key = tag.get("name", {decode: true, string: true} )
57 | const value = tag.get("value", {decode: true, string: true})
58 | tagsMap.set(key, value)
59 | }
60 |
61 | if (! tagsMap.has("Content-Type")) {
62 | throw new ContractError(`uncorrect data transaction`)
63 | }
64 |
65 | if (! tagsMap.get("Content-Type").startsWith("image/") ) {
66 | throw new ContractError(`invalid mime type`)
67 | }
68 |
69 | // ------------------------>
70 |
71 | podcasts.push( {
72 | "pid": pid,
73 | "index": _getPodcastIndex(), // id equals the index of the podacast obj in the podcasts array
74 | "podcastName": name,
75 | "description": desc,
76 | "cover": cover,
77 | "episodes":[],
78 | "logs": [pid]
79 | } )
80 |
81 | return { state }
82 | }
83 |
84 | if ( input.function === "addEpisode") {
85 | const index = input.index // podcast index
86 | const name = input.name
87 | const audio = input.audio
88 | const desc = input.desc
89 |
90 | const tagsMap = new Map()
91 |
92 | if (caller !== contractOwner ) {
93 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
94 | }
95 |
96 | if (! Number.isInteger(index) ) {
97 | throw new ContractError(`index must be an integer`)
98 | }
99 |
100 | if (! podcasts[index]) {
101 | throw new ContractError(`podcast having index: ${index} not found`)
102 | }
103 |
104 |
105 | if (typeof name !== "string" || name.length > 50) {
106 | throw new ContractError(`uncorrect name limit`)
107 | }
108 |
109 | if (typeof desc !== "string" || desc.length > 250) {
110 | throw new ContractError(`description too long`)
111 | }
112 |
113 | if (typeof audio !== "string" || audio.length !== 43) {
114 | throw new ContractError(`invalid audio TX`)
115 | }
116 |
117 | const audioTx = await SmartWeave.unsafeClient.transactions.get(audio)
118 | const tags = audioTx.get("tags")
119 |
120 | for (let tag of tags) {
121 | const key = tag.get("name", {decode: true, string: true} )
122 | const value = tag.get("value", {decode: true, string: true})
123 | tagsMap.set(key, value)
124 | }
125 |
126 | if (! tagsMap.has("Content-Type")) {
127 | throw new ContractError(`uncorrect data transaction`)
128 | }
129 |
130 | if (! tagsMap.get("Content-Type").startsWith("audio/") ) {
131 | throw new ContractError(`invalid mime type`)
132 | }
133 |
134 | podcasts[index]["episodes"].push({
135 | "eid": SmartWeave.transaction.id, // episode TXID
136 | "childOf": index,
137 | "episodeName": name,
138 | "description": desc,
139 | "audioTx": audio,
140 | "uploadedAt": SmartWeave.block.height,
141 | "logs": [SmartWeave.transaction.id]
142 | })
143 |
144 | return { state }
145 |
146 | }
147 |
148 |
149 | // PODCAST ACTIONS:
150 |
151 | if (input.function === "deletePodcast") {
152 | const index = input.index //podcast index
153 |
154 | if ( caller !== contractOwner) {
155 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
156 | }
157 |
158 | if (! Number.isInteger(index) ) {
159 | throw new ContractError(`index must be an integer`)
160 | }
161 |
162 | if (! podcasts[index]) {
163 | throw new ContractError(`podcast having index: ${index} does not exist`)
164 | }
165 |
166 | podcasts.splice(index, 1)
167 |
168 | return { state }
169 | }
170 |
171 | if (input.function === "editPodcastName") {
172 | const index = input.index
173 | const name = input.name
174 |
175 | const actionTx = SmartWeave.transaction.id
176 |
177 | if ( caller !== contractOwner) {
178 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
179 | }
180 |
181 | if (! Number.isInteger(index) ) {
182 | throw new ContractError(`index must be an integer`)
183 | }
184 |
185 | if (! podcasts[index]) {
186 | throw new ContractError(`podcast having index: ${index} does not exist`)
187 | }
188 |
189 | if (typeof name !== "string") {
190 | throw new ContractError(`invalid name type`)
191 | }
192 |
193 | if ( name.length < 3 || name.length > 50 ) {
194 | throw new ContractError(`the name does not meet the name limits`)
195 | }
196 |
197 | if ( podcasts[index]["podcastName"] === name) {
198 | throw new ContractError(`old name and new name cannot be equals`)
199 | }
200 |
201 | podcasts[index]["podcastName"] = name
202 | podcasts[index]["logs"].push(actionTx)
203 |
204 | return { state }
205 | }
206 |
207 | if (input.function === "editPodcastDesc") {
208 | const index = input.index
209 | const desc = input.desc
210 |
211 | const actionTx = SmartWeave.transaction.id
212 |
213 | if ( caller !== contractOwner) {
214 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
215 | }
216 |
217 | if (! Number.isInteger(index) ) {
218 | throw new ContractError(`index must be an integer`)
219 | }
220 |
221 | if (! podcasts[index]) {
222 | throw new ContractError(`podcast having index: ${index} does not exist`)
223 | }
224 |
225 | if ( typeof desc !== "string" ) {
226 | throw new ContractError(`invalid description type`)
227 | }
228 |
229 | if ( desc.length > 250 ) {
230 | throw new ContractError(`description length too high`)
231 | }
232 |
233 | if ( podcasts[index]["description"] === desc ) {
234 | throw new ContractError(`old description and new description cannot be equals`)
235 | }
236 |
237 | podcasts[index]["description"] = desc
238 | podcasts[index]["logs"].push(actionTx)
239 |
240 | return { state }
241 |
242 | }
243 |
244 | if (input.function === "editPodcastCover") {
245 | const index = input.index
246 | const cover = input.cover
247 |
248 | const tagsMap = new Map();
249 | const actionTx = SmartWeave.transaction.id
250 |
251 | if ( caller !== contractOwner) {
252 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
253 | }
254 |
255 | if (! Number.isInteger(index) ) {
256 | throw new ContractError(`index must be an integer`)
257 | }
258 |
259 | if (! podcasts[index]) {
260 | throw new ContractError(`podcast having index: ${index} does not exist`)
261 | }
262 |
263 | if (typeof cover !== "string" || cover.length !== 43) {
264 | throw new ContractError(`uncorrect cover format`)
265 | }
266 |
267 | const coverTx = await SmartWeave.unsafeClient.transactions.get(cover)
268 | const tags = coverTx.get("tags")
269 |
270 | for (let tag of tags) {
271 | const key = tag.get("name", {decode: true, string: true} )
272 | const value = tag.get("value", {decode: true, string: true})
273 | tagsMap.set(key, value)
274 | }
275 |
276 | if (! tagsMap.has("Content-Type")) {
277 | throw new ContractError(`uncorrect data transaction`)
278 | }
279 |
280 | if (! tagsMap.get("Content-Type").startsWith("image/") ) {
281 | throw new ContractError(`invalid mime type`)
282 | }
283 |
284 | if ( podcasts[index]["cover"] === cover ) {
285 | throw new ContractError(`old cover and new cover cannot be equals`)
286 | }
287 |
288 | podcasts[index]["cover"] = cover
289 | podcasts[index]["logs"].push(actionTx)
290 |
291 | return { state }
292 |
293 | }
294 |
295 | // EPISODES ACTIONS:
296 |
297 | if (input.function === "editEpisodeName") {
298 | const name = input.name
299 | const index = input.index // podcast index
300 | const id = input.id // episode index
301 |
302 | const actionTx = SmartWeave.transaction.id
303 |
304 | if (caller !== contractOwner) {
305 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
306 | }
307 |
308 | if (! podcasts[index]) {
309 | throw new ContractError(`podcast having index: ${index} not found`)
310 | }
311 |
312 | if (! podcasts[index]["episodes"][id]) {
313 | throw new ContractError(`episode having index: ${id} not found`)
314 | }
315 |
316 | if (typeof name !== "string") {
317 | throw new ContractError(`invalid name format`)
318 | }
319 |
320 | if (name.length < 2 || name.length > 50) {
321 | throw new ContractError(`${name} does not meet the name limits`)
322 | }
323 |
324 | if ( podcasts[index]["episodes"][id]["episodeName"] === name ) {
325 | throw new ContractError(`new name and old name cannot be the same`)
326 | }
327 |
328 | podcasts[index]["episodes"][id]["episodeName"] = name
329 | podcasts[index]["episodes"][id]["logs"].push(actionTx)
330 |
331 | return { state }
332 | }
333 |
334 | if (input.function === "editEpisodeDesc") {
335 | const index = input.index
336 | const id = input.id
337 | const desc = input.desc
338 |
339 | const actionTx = SmartWeave.transaction.id
340 |
341 | if (caller !== contractOwner) {
342 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
343 | }
344 |
345 | if (! podcasts[index]) {
346 | throw new ContractError(`podcast having index: ${index} not found`)
347 | }
348 |
349 | if (! podcasts[index]["episodes"][id]) {
350 | throw new ContractError(`episode having index: ${id} not found`)
351 | }
352 |
353 | if (typeof desc !== "string") {
354 | throw new ContractError(`invalid description format`)
355 | }
356 |
357 | if ( desc.length < 25 || desc.length > 500 ) {
358 | throw new ContractError(`the description text does not meet the desc limits`)
359 | }
360 |
361 | if ( podcasts[index]["episodes"][id]["description"] === desc) {
362 | throw new ContractError(`old description and new description canot be the same`)
363 | }
364 |
365 | podcasts[index]["episodes"][id]["description"] = desc
366 | podcasts[index]["episodes"][id]["logs"].push(actionTx)
367 |
368 | return { state }
369 | }
370 |
371 | if (input.function === "deleteEpisode") {
372 | const index = input.index
373 | const id = input.id
374 |
375 | if ( caller !== contractOwner) {
376 | throw new ContractError(`invalid caller. Only ${contractOwner} can perform this action`)
377 | }
378 |
379 | if (! podcasts[index]) {
380 | throw new ContractError(`podcast having ID: ${index} not found`)
381 | }
382 |
383 | if (! podcasts[index][id]) {
384 | throw new ContractError(`episode having index: ${id} not found`)
385 | }
386 |
387 | podcasts[index]["episodes"].splice(id, 1)
388 |
389 | return { state }
390 | }
391 |
392 | // HELPER FUNCTIONS:
393 | function _getPodcastIndex() {
394 | if (podcasts.length === 0) {
395 | return 0
396 | }
397 |
398 | return (podcasts.length - 1 )
399 | }
400 |
401 |
402 | throw new ContractError(`unknow function supplied: '${input.function}'`)
403 |
404 | }
405 |
406 |
--------------------------------------------------------------------------------
/podcasts/podcast.json:
--------------------------------------------------------------------------------
1 | {
2 | "podcasts": []
3 | }
4 |
--------------------------------------------------------------------------------
/podcasts/podcastNew.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SWC used as first level data registery for
3 | * Arweave hosted podcasts.
4 | *
5 | * The current contract represents a basic PoC
6 | *
7 | * contributor(s): charmful0x
8 | *
9 | * Lisence: MIT
10 | **/
11 |
12 |
13 |
14 | export async function handle(state, action) {
15 | const input = action.input
16 | const caller = action.caller
17 | const podcasts = state.podcasts
18 |
19 | // ERRORS List
20 | const ERROR_INVALID_CALLER = `the caller is not allowed to execute this function`;
21 | const ERROR_INVALID_PRIMITIVE_TYPE = `the given data is not a corrected primitive type per function`;
22 | const ERROR_INVALID_STRING_LENGTH = `the string is out of the allowed length ranges`;
23 | const ERROR_NOT_A_DATA_TX = `the transaction is not an Arweave TX DATA`;
24 | const ERROR_MIME_TYPE = `the given mime type is not supported`;
25 | const ERROR_UNSUPPORTED_LANG = `the given language code is not supported`;
26 | const ERROR_REQUIRED_PARAMETER = `the function still require a parameter`;
27 | const ERROR_INVALID_NUMBER_TYPE = `only inetegers are allowed`;
28 | const ERROR_NEGATIVE_INTEGER = `negative integer was supplied when only positive Intare allowed`;
29 | const ERROR_EPISODE_INDEX_NOT_FOUND = `there is no episode with the given index`;
30 | const ERROR_PODCAST_INDEX_NOT_FOUND = `there is no podcast with the given index`;
31 | const ERROR_OLD_VALUE_EQUAL_TO_NEW = `old valueand new value are equal`;
32 |
33 |
34 | if (input.function === "createPodcast") {
35 | const name = input.name
36 | const author = input.author
37 | const desc = input.desc
38 | const lang = input.lang
39 | const isExplicit = input.isExplicit
40 | const categories = input.categories
41 | const email = input.email
42 | const cover = input.cover
43 |
44 | const pid = SmartWeave.transaction.id
45 |
46 |
47 | await _getContractOwner(true, caller)
48 |
49 | // show-level string validation
50 |
51 | _validateStringTypeLen(name, 3, 400);
52 | _validateStringTypeLen(author, 2, 50)
53 | _validateStringTypeLen(desc, 10, 4000);
54 | _validateStringTypeLen(email, 0, 320);
55 | _validateStringTypeLen(categories, 3, 150);
56 | _validateStringTypeLen(cover, 43, 43);
57 | _validateStringTypeLen(lang, 2, 2);
58 |
59 |
60 | await _validateDataTransaction(cover, "image/");
61 |
62 | if (! ["yes", "no"].includes(isExplicit)) {
63 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE)
64 | }
65 |
66 |
67 | podcasts.push({
68 | pid: pid,
69 | index: _getPodcastIndex(), // id equals the index of the podacast obj in the podcasts array
70 | childOd: SmartWeave.contract.id,
71 | owner: caller,
72 | podcastName: name,
73 | author: author,
74 | email: email,
75 | description: desc,
76 | language: lang,
77 | explicit: isExplicit,
78 | categories: (categories.split(",")).map(category => category.trim()),
79 | cover: cover,
80 | episodes:[],
81 | logs: [pid]
82 | })
83 |
84 | return { state }
85 | }
86 |
87 | if ( input.function === "addEpisode") {
88 | const index = input.index // podcasts index
89 | const name = input.name
90 | const audio = input.audio // the TXID of 'audio/' data
91 | const desc = input.desc
92 |
93 | await _getContractOwner(true, caller);
94 |
95 | // show-level string validation
96 |
97 | _validateStringTypeLen(name, 3, 4000);
98 | _validateStringTypeLen(audio, 43, 43);
99 | _validateStringTypeLen(desc, 0, 4000);
100 | _validateInteger(index, true)
101 |
102 | const TxMetadata = await _validateDataTransaction(audio, "audio/")
103 |
104 |
105 | if (! podcasts[index]) {
106 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND)
107 | }
108 |
109 |
110 | podcasts[index]["episodes"].push({
111 | eid: SmartWeave.transaction.id,
112 | childOf: index,
113 | episodeName: name,
114 | description: desc,
115 | audioTx: audio,
116 | audioTxByteSize: Number.parseInt( TxMetadata.size ),
117 | type: TxMetadata.type,
118 | uploadedAt: SmartWeave.block.timestamp,
119 | logs: [SmartWeave.transaction.id]
120 | })
121 |
122 | return { state }
123 |
124 | }
125 |
126 |
127 |
128 | // PODCAST ACTIONS:
129 |
130 | if (input.function === "deletePodcast") {
131 | const index = input.index
132 |
133 | await _getContractOwner(true, caller);
134 | _validateInteger(index, true);
135 |
136 | if (! podcasts[index]) {
137 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND)
138 | }
139 |
140 | podcasts.splice(index, 1)
141 |
142 | return { state }
143 | }
144 |
145 | if (input.function === "editPodcastName") {
146 | const index = input.index
147 | const name = input.name
148 |
149 | const actionTx = SmartWeave.transaction.id
150 |
151 | await _getContractOwner(true, caller);
152 | _validateStringTypeLen(name, 3, 50);
153 | _validateInteger(index, true);
154 |
155 | if (! podcasts[index]) {
156 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND)
157 | }
158 |
159 | if ( podcasts[index]["podcastName"] === name) {
160 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW)
161 | }
162 |
163 | podcasts[index]["podcastName"] = name
164 | podcasts[index]["logs"].push(actionTx)
165 |
166 | return { state }
167 | }
168 |
169 | if (input.function === "editPodcastDesc") {
170 | const index = input.index
171 | const desc = input.desc
172 |
173 | const actionTx = SmartWeave.transaction.id
174 |
175 | await _getContractOwner(true, caller);
176 | _validateInteger(true, index);
177 | _validateStringTypeLen(desc, 10, 750);
178 |
179 | if (! podcasts[index]) {
180 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND)
181 | }
182 |
183 | if ( podcasts[index]["description"] === desc ) {
184 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW)
185 | }
186 |
187 | podcasts[index]["description"] = desc
188 | podcasts[index]["logs"].push(actionTx)
189 |
190 | return { state }
191 |
192 | }
193 |
194 | if (input.function === "editPodcastCover") {
195 | const index = input.index
196 | const cover = input.cover
197 | const actionTx = SmartWeave.transaction.id
198 | const tagsMap = new Map();
199 |
200 | await _getContractOwner(true, caller);
201 | _validateStringTypeLen(cover, 43, 43);
202 | _validateInteger(true, index);
203 |
204 | if (! podcasts[index]) {
205 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND)
206 | }
207 |
208 | if ( podcasts[index]["cover"] === cover ) {
209 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW)
210 | }
211 |
212 | await _validateDataTransaction(cover)
213 |
214 | podcasts[index]["cover"] = cover
215 | podcasts[index]["logs"].push(actionTx)
216 |
217 | return { state }
218 |
219 | }
220 |
221 |
222 |
223 |
224 | // EPISODES ACTIONS:
225 |
226 | if (input.function === "editEpisodeName") {
227 | const name = input.name
228 | const index = input.index //podcast index
229 | const id = input.id // episode's index
230 |
231 | const actionTx = SmartWeave.transaction.id
232 |
233 | await _getContractOwner(true, caller);
234 |
235 | _validateStringTypeLen(name, 2, 50);
236 | _validateInteger(index, true);
237 | _validateInteger(id, true);
238 | _validateEpisodeExistence(index, id)
239 |
240 | if ( podcasts[index]["episodes"][id]["episodeName"] === name ) {
241 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW)
242 | }
243 |
244 | podcasts[index]["episodes"][id]["episodeName"] = name
245 | podcasts[index]["episodes"][id]["logs"].push(actionTx)
246 |
247 |
248 | return { state }
249 | }
250 |
251 | if (input.function === "editEpisodeDesc") {
252 | const index = input.index
253 | const id = input.id
254 | const desc = input.desc
255 |
256 | const actionTx = SmartWeave.transaction.id
257 |
258 | await _getContractOwner(true, caller);
259 |
260 | _validateStringTypeLen(desc, 25, 500);
261 | _validateInteger(index, true);
262 | _validateInteger(id, true);
263 | _validateEpisodeExistence(index, id);
264 |
265 |
266 | if ( podcasts[index]["episodes"][id]["description"] === desc) {
267 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW)
268 | }
269 |
270 | podcasts[index]["episodes"][id]["description"] = desc
271 | podcasts[index]["episodes"][id]["logs"].push(actionTx)
272 |
273 | return { state }
274 | }
275 |
276 | if (input.function === "deleteEpisode") {
277 | const index = input.index
278 | const id = input.id
279 |
280 | await _getContractOwner(true, caller);
281 |
282 | _validateInteger(index, true);
283 | _validateInteger(id, true);
284 | _validateEpisodeExistence(index, id)
285 |
286 | podcasts[index]["episodes"].splice(id, 1)
287 |
288 | return { state }
289 | }
290 |
291 | // HELPER FUNCTIONS:
292 | function _getPodcastIndex() {
293 | if (podcasts.length === 0) {
294 | return 0
295 | }
296 |
297 | return (podcasts.length - 1 )
298 | };
299 |
300 | function _validateStringTypeLen(str, minLen, maxLen) {
301 |
302 | if (typeof str !== "string") {
303 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE)
304 | }
305 |
306 | if (str.length < minLen || str.length > maxLen) {
307 | throw new ContractError(ERROR_INVALID_STRING_LENGTH)
308 | }
309 | };
310 |
311 | function _validateInteger(number, allowNull) {
312 |
313 | if ( typeof allowNull === "undefined" ) {
314 | throw new ContractError(ERROR_REQUIRED_PARAMETER)
315 | }
316 |
317 | if (! Number.isInteger(number) ) {
318 | throw new ContractError(ERROR_INVALID_NUMBER_TYPE)
319 | }
320 |
321 | if (allowNull) {
322 | if (number < 0) {
323 | throw new ContractError(ERROR_NEGATIVE_INTEGER)
324 | }
325 | } else if (number <= 0) {
326 | throw new ContractError(ERROR_INVALID_NUMBER_TYPE)
327 | }
328 | };
329 |
330 | async function _getContractOwner(validate, caller) {
331 |
332 | const contractID = SmartWeave.contract.id
333 | const contractTxObject = await SmartWeave.unsafeClient.transactions.get(contractID)
334 | const base64Owner = contractTxObject["owner"]
335 | const contractOwner = await SmartWeave.unsafeClient.wallets.ownerToAddress(base64Owner)
336 |
337 | if (validate && (contractOwner !== caller)) {
338 | throw new ContractError(ERROR_INVALID_CALLER)
339 | }
340 |
341 | return contractOwner
342 | }
343 |
344 | async function _validateDataTransaction(tx, mimeType) {
345 |
346 | const tagsMap = new Map();
347 | const transaction = await SmartWeave.unsafeClient.transactions.get(tx)
348 | const tags = transaction.get("tags")
349 |
350 | for (let tag of tags) {
351 | const key = tag.get("name", {decode: true, string: true} )
352 | const value = tag.get("value", {decode: true, string: true})
353 | tagsMap.set(key, value)
354 | }
355 |
356 | if (! tagsMap.has("Content-Type")) {
357 | throw new ContractError(ERROR_NOT_A_DATA_TX)
358 | }
359 |
360 | if (! tagsMap.get("Content-Type").startsWith(mimeType) ) {
361 | throw new ContractError(ERROR_MIME_TYPE)
362 | }
363 |
364 | return {
365 | size: transaction.data_size,
366 | type: tagsMap.get("Content-Type")
367 | }
368 |
369 | };
370 |
371 |
372 | function _validateEpisodeExistence(index, id) {
373 |
374 | if (! podcasts[index]) {
375 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND)
376 | }
377 |
378 | if (! podcasts[index]["episodes"][id]) {
379 | throw new ContractError(ERROR_EPISODE_INDEX_NOT_FOUND)
380 | }
381 | }
382 |
383 |
384 | throw new ContractError(`unknow function supplied: ${input.function}`)
385 | }
386 |
--------------------------------------------------------------------------------
/public/alt-favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Parallel-news/permacast/384bc1fcd91d4e1e3468e7afe807e35fab8d84ec/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | permacast
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/public/locales/en/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "Language",
3 | "navbar": {
4 | "help": "Get help",
5 | "new": "What's new",
6 | "swal": {
7 | "title": "New in permacast V3 ✨",
8 | "html": "iTunes/Spotify compatible RSS imports via the upcoming permacast CLIGas cost reductionAuto-protection from duplicated uploadsRead full changelog"
9 | }
10 | },
11 | "connector": {
12 | "login": "ArConnect login",
13 | "logout": "Logout",
14 | "swal": {
15 | "title": "Install ArConnect to continue",
16 | "text": "Permablog uses ArConnect to make it easier to authenticate and send transactions for questions and answers",
17 | "footer": "Download ArConnect here"
18 | }
19 | },
20 | "uploadshow": {
21 | "addpoadcast": "Add a podcast",
22 | "title": "Add a new show",
23 | "label": "You'll add episodes to the show next.",
24 | "name": "Show name",
25 | "description": "Show description",
26 | "image": "Cover image",
27 | "author": "Author",
28 | "email": "Email",
29 | "language": "Podcast language",
30 | "category": "Category",
31 | "explicit": "Contains explicit content",
32 | "upload": "Upload",
33 | "cancel": "Cancel",
34 | "feeText": "Uploading the show will cost: ",
35 | "swal": {
36 | "showadded": {
37 | "title": "Show added",
38 | "text": "Show added permanently to Arweave. Check in a few minutes after the transaction has mined."
39 | },
40 | "uploadfailed": {
41 | "title": "Unable to add show",
42 | "text": "Check your wallet balance and network connection"
43 | },
44 | "reset": {
45 | "text": "Podcast cover image is not squared (1:1 aspect ratio)!"
46 | },
47 | "uploading": {
48 | "title": "Uploading, please wait a few seconds..."
49 | }
50 | },
51 | "uploading": "Processing..."
52 | },
53 | "podcasthtml": {
54 | "swal": {
55 | "title": "Coming soon",
56 | "text": "Tip your favorite podcasts with $NEWS to show support"
57 | }
58 | },
59 | "uploadepisode": {
60 | "swal": {
61 | "upload": {
62 | "title": "Upload underway!",
63 | "text": "We'll let you know when it's done. Go grab a ☕ or 🍺"
64 | },
65 | "uploadcomplete": {
66 | "title": "Upload complete",
67 | "text": "Episode uploaded permanently to Arweave. Check in a few minutes after the transaction has mined."
68 | },
69 | "uploadfailed": {
70 | "title": "Upload failed",
71 | "text": "Check your AR balance and network connection"
72 | }
73 | },
74 | "title": "Add new episode to",
75 | "name": "Episode name",
76 | "description": "Episode description",
77 | "file": "Audio file",
78 | "verto": "List as an Atomic NFT on Verto?",
79 | "toupload": "to upload",
80 | "upload": "Upload",
81 | "uploading": "Uploading, please wait...",
82 | "uploaded": "Uploaded",
83 | "feeText": "Uploading an episode will incur an additional fee of "
84 | },
85 | "generalerrors": {
86 | "cantfindaddress": {
87 | "title": "Unable to find address",
88 | "text": "Make sure your address exists and re-connect the wallet"
89 | },
90 | "cantfetchprices": {
91 | "title": "Unable to fetch storage prices",
92 | "text": "Check your internet connection and try again later"
93 | },
94 | "lowbalance": {
95 | "title": "Low balance",
96 | "text": "You don't have enough AR to cover the transaction fee. This transaction will cost: "
97 | }
98 | },
99 | "podcast": {
100 | "newepisode": "add new episode"
101 | },
102 | "loading": "Loading podcasts...",
103 | "nopodcasts": "No podcasts here yet. Upload one!",
104 | "sortpodcastsby": "Sort podcasts by",
105 | "sorting": {
106 | "podcastsactivity": "Latest Active podcast",
107 | "episodescount": "Episodes Count"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/public/locales/uk/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "Мова",
3 | "navbar": {
4 | "help": "Отримати допомогу",
5 | "new": "Що нового",
6 | "swal": {
7 | "title": "Нове у permacast V3 ✨",
8 | "html": "iTunes/Spotify сумісні RSS імпортуються через майбутній permacast CLIЗниження вартості комісії за газАвтоматичний захист від завантаження дублікатівПрочитай повний список змін"
9 | }
10 | },
11 | "connector": {
12 | "login": "ArConnect логін",
13 | "logout": "Вийти",
14 | "swal": {
15 | "title": "Інсталюй ArConnect, щоб продовжити",
16 | "text": "Permablog використовує ArConnect, щоб полегшити автентифікацію та відправку запитів та відповідей для транзакцій",
17 | "footer": "Завантаж ArConnect тут"
18 | }
19 | },
20 | "uploadshow": {
21 | "addpoadcast": "Додай подкаст",
22 | "title": "Додай нове шоу",
23 | "label": "Ви додасте епізоди до шоу далі.",
24 | "name": "Назва шоу",
25 | "description": "Опис шоу",
26 | "image": "Обкладинка",
27 | "author": "Автор",
28 | "email": "Електронна пошта",
29 | "language": "Мова подкасту",
30 | "category": "Категорія",
31 | "explicit": "Містить відвертий контент",
32 | "upload": "Завантажити",
33 | "cancel": "Відмінити",
34 | "feeText": "Завантаження шоу буде коштуватиt: ",
35 | "swal": {
36 | "showadded": {
37 | "title": "Шоу додано",
38 | "text": "Шоу остаточно додано до Arweave. Перевір через кілька хвилин після видобутку транзакції."
39 | },
40 | "uploadfailed": {
41 | "title": "Не вдається додати шоу",
42 | "text": "Перевір баланс гаманця та підключення до мережі"
43 | },
44 | "reset": {
45 | "text": "Обкладинка подкасту не квадратна (співвідношення сторін 1:1)!"
46 | },
47 | "uploading": {
48 | "title": "Завантаження триває, почекай кілька секунд..."
49 | }
50 | },
51 | "uploading": "Обробка..."
52 | },
53 | "podcasthtml": {
54 | "swal": {
55 | "title": "Незабаром",
56 | "text": "Підтримай монеткою свої улюблені подкасти з $NEWS"
57 | }
58 | },
59 | "uploadepisode": {
60 | "swal": {
61 | "upload": {
62 | "title": "Завантаження розпочалося!",
63 | "text": "Ми повідомимо, коли воно завершиться. Попий поки ☕ чи 🍺"
64 | },
65 | "uploadcomplete": {
66 | "title": "Завантаження завершено",
67 | "text": "Епізод остаточно завантажено до Arweave. Перевір через кілька хвилин після видобутку транзакції."
68 | },
69 | "uploadfailed": {
70 | "title": "Помилка завантаження",
71 | "text": "Перевір баланс AR та підключення до мережі"
72 | }
73 | },
74 | "title": "Додай новий епізод до",
75 | "name": "Назва епізоду",
76 | "description": "Опис епізоду",
77 | "file": "Аудіо файл",
78 | "verto": "Монетизувати як Atomic NFT на Verto?",
79 | "toupload": "до завантаження",
80 | "upload": "Завантажити",
81 | "uploading": "Завантаження триває, будь ласка, почекай...",
82 | "uploaded": "Завантажено",
83 | "feeText": "За завантаження епізоду буде стягнуто додаткову оплату у розмірі"
84 | },
85 | "generalerrors": {
86 | "cantfindaddress": {
87 | "title": "Не можливо знайти адресу",
88 | "text": "Переконайся, що адреса існує та повторно підключись до гаманця"
89 | },
90 | "cantfetchprices": {
91 | "title": "Не вдалося отримати ціни на сховище",
92 | "text": "Перевір інтернет-з’єднання та спробуй пізніше"
93 | },
94 | "lowbalance": {
95 | "title": "Недостатній баланс",
96 | "text": "У тебе недостатньо AR щоб покрити витрати на транзакцію. Ця транзакція коштуватиме:"
97 | }
98 | },
99 | "podcast": {
100 | "newepisode": "додай новий епізод"
101 | },
102 | "loading": "Завантажую подкасти...",
103 | "nopodcasts": "Тут ще немає подкастів. Завантаж свій!",
104 | "sortpodcastsby": "Сортувати подкасти за",
105 | "sorting": {
106 | "podcastsactivity": "Останній Активний подкаст",
107 | "episodescount": "Кількість Епізодів"
108 | }
109 | }
--------------------------------------------------------------------------------
/public/locales/zh/translation.json:
--------------------------------------------------------------------------------
1 | {
2 | "language": "语言",
3 | "navbar": {
4 | "help": "获取帮助",
5 | "new": "新功能",
6 | "swal": {
7 | "title": "Permacast V3 新功能 ✨",
8 | "html": "通过即将推出的 Permacast CLI 导入 iTunes/Spotify 兼容的 RSS降低 Gas 成本防止重复上传的自动保护措施阅读完整的更新日志"
9 | }
10 | },
11 | "connector": {
12 | "login": "ArConnect 登录",
13 | "logout": "登出",
14 | "swal": {
15 | "title": "安装 ArConnect 以继续",
16 | "text": "Permacast 使用 ArConnect 让认证和发送交易更加容易",
17 | "footer": "下载 ArConnect"
18 | }
19 | },
20 | "uploadshow": {
21 | "addpoadcast": "添加播客",
22 | "title": "添加新的节目",
23 | "label": "之后您可在节目中添加剧集",
24 | "name": "节目名称",
25 | "description": "节目描述",
26 | "image": "封面图片",
27 | "author": "作者",
28 | "email": "邮箱",
29 | "language": "播客语言",
30 | "category": "类别",
31 | "explicit": "少儿不宜",
32 | "upload": "上传",
33 | "cancel": "取消",
34 | "feeText": "上传节目的费用:",
35 | "swal": {
36 | "showadded": {
37 | "title": "节目已添加",
38 | "text": "节目已永久地上传到 Arweave,待交易完成几分钟后再来查看"
39 | },
40 | "uploadfailed": {
41 | "title": "无法添加节目",
42 | "text": "检查您的 AR 余额和网络连接"
43 | },
44 | "reset": {
45 | "text": "播客的封面图片必须为正方形(1:1的长宽比)!"
46 | },
47 | "uploading": {
48 | "title": "上传中,请稍候..."
49 | }
50 | },
51 | "uploading": "加工..."
52 | },
53 | "podcasthtml": {
54 | "swal": {
55 | "title": "即将到来",
56 | "text": "支持并为您最喜爱的播客打赏 $NEWS"
57 | }
58 | },
59 | "uploadepisode": {
60 | "swal": {
61 | "upload": {
62 | "title": "上传中",
63 | "text": "我们会在上传完成后通知您,来一杯 ☕ 或 🍺"
64 | },
65 | "uploadcomplete": {
66 | "title": "上传完成",
67 | "text": "剧集已永久地上传到 Arweave,待交易完成几分钟后再来查看"
68 | },
69 | "uploadfailed": {
70 | "title": "上传失败",
71 | "text": "检查您的 AR 余额和网络连接"
72 | }
73 | },
74 | "title": "添加新的剧集到",
75 | "name": "剧集名称",
76 | "description": "剧集描述",
77 | "file": "音频文件",
78 | "verto": "是否在 Verto 上展示原子 NFT ?",
79 | "toupload": "来进行上传",
80 | "upload": "上传",
81 | "uploading": "上传中,请耐心等待...",
82 | "uploaded": "已完成",
83 | "feeText": "上传剧集将产生额外费用"
84 | },
85 | "generalerrors": {
86 | "cantfindaddress": {
87 | "title": "找不到地址",
88 | "text": "尝试重新连接您的钱包并更改其访问配置"
89 | },
90 | "cantfetchprices": {
91 | "title": "无法获取存储价格",
92 | "text": "检查您的互联网连接,稍后再试"
93 | },
94 | "lowbalance": {
95 | "title": "余额不足",
96 | "text": "您没有足够的 AR 来支付交易费用。 该交易将花费:"
97 | }
98 | },
99 | "podcast": {
100 | "newepisode": "添加新剧集"
101 | },
102 | "loading": "正在加载播客...",
103 | "nopodcasts": "这里还没有播客,请上传一个!",
104 | "sortpodcastsby": "播客排序",
105 | "sorting": {
106 | "podcastsactivity": "最新收听最多的播客",
107 | "episodescount": "剧集数"
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Parallel-news/permacast/384bc1fcd91d4e1e3468e7afe807e35fab8d84ec/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Parallel-news/permacast/384bc1fcd91d4e1e3468e7afe807e35fab8d84ec/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import { HashRouter as Router, Route } from "react-router-dom";
2 | import NavBar from "./component/navbar.jsx";
3 | import Podcast from "./component/podcast.jsx";
4 | import Index from "./component/index.jsx";
5 | import PodcastRss from "./component/podcast_rss.jsx";
6 |
7 | export default function App() {
8 | return (
9 |
10 |
11 |
12 | }
16 | />
17 | } />
18 | }
22 | />
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import { render, screen } from '@testing-library/react';
2 | import App from './App';
3 |
4 | test('renders learn react link', () => {
5 | render();
6 | const linkElement = screen.getByText(/learn react/i);
7 | expect(linkElement).toBeInTheDocument();
8 | });
9 |
--------------------------------------------------------------------------------
/src/component/arconnect_loader.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import UploadShow from './upload_show.jsx'
3 | import Swal from 'sweetalert2'
4 | import { useTranslation } from 'react-i18next'
5 |
6 | const requiredPermissions = ['ACCESS_ADDRESS', 'ACCESS_ALL_ADDRESSES', 'SIGNATURE', 'SIGN_TRANSACTION']
7 |
8 | export default function Header() {
9 | const [walletConnected, setWalletConnected] = useState(false)
10 | const [address, setAddress] = useState(undefined)
11 | const [ansData, setANSData] = useState(undefined)
12 | const { t } = useTranslation()
13 |
14 | useEffect(() => {
15 | // add ArConnect event listeners
16 | window.addEventListener('arweaveWalletLoaded', walletLoadedEvent)
17 | window.addEventListener('walletSwitch', walletSwitchEvent)
18 | return () => {
19 | // remove ArConnect event listeners
20 | window.removeEventListener('arweaveWalletLoaded', walletLoadedEvent)
21 | window.removeEventListener('walletSwitch', walletSwitchEvent)
22 | }
23 | })
24 |
25 | // wallet address change event
26 | // when the user switches wallets
27 | const walletSwitchEvent = async (e) => {
28 | setAddress(e.detail.address)
29 | // setAddress("ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U")
30 | // setANSData(await getANSLabel(e.detail.address))
31 | }
32 |
33 | // ArConnect script injected event
34 | const walletLoadedEvent = async () => {
35 | try {
36 | // connected, set address
37 | // (the user can still be connected, but
38 | // for this actions the "ACCESS_ADDRESS"
39 | // permission is required. if the user doesn't
40 | // have that, we still need to ask them to connect)
41 | const addr = await getAddr()
42 | setAddress(addr)
43 | // setAddress("ljvCPN31XCLPkBo9FUeB7vAK0VC6-eY52-CS-6Iho8U")
44 | // setANSData(await getANSLabel(addr))
45 | setWalletConnected(true)
46 | } catch {
47 | // not connected
48 | setAddress(undefined)
49 | setWalletConnected(false)
50 | }
51 | }
52 |
53 | const installArConnectAlert = () => {
54 | Swal.fire({
55 | icon: "warning",
56 | title: t("connector.swal.title"),
57 | text: t("connector.swal.text"),
58 | footer: `${t("connector.swal.footer")}`,
59 | customClass: "font-mono",
60 | })
61 | }
62 |
63 | const getAddr = () => window.arweaveWallet.getActiveAddress()
64 |
65 | const shortenAddress = (addr) => {
66 | if (addr) {
67 | return addr.substring(0, 4) + '...' + addr.substring(addr.length - 4)
68 | }
69 | return addr
70 | }
71 |
72 | // const getANSLabel = async (addr) => {
73 |
74 | // return ans?.currentLabel
75 | // }
76 |
77 | useEffect(() => {
78 | const fetchData = async () => {
79 | try {
80 | const response = await fetch(`https://ans-testnet.herokuapp.com/profile/${address}`)
81 | const ans = await response.json()
82 | const {address_color, currentLabel, avatar = ""} = ans;
83 | console.log({address_color, currentLabel, avatar})
84 | setANSData({address_color, currentLabel, avatar})
85 | } catch (error) {
86 | console.error(error)
87 | }
88 | };
89 |
90 | fetchData();
91 | }, [address]);
92 |
93 | const arconnectConnect = async () => {
94 | if (window.arweaveWallet) {
95 | try {
96 | await window.arweaveWallet.connect(requiredPermissions)
97 | setAddress(await getAddr())
98 | setWalletConnected(true)
99 |
100 | } catch { }
101 | } else {
102 | installArConnectAlert()
103 | }
104 | }
105 |
106 | const arconnectDisconnect = async () => {
107 | await window.arweaveWallet.disconnect()
108 | setWalletConnected(false)
109 | setAddress(undefined)
110 | }
111 |
112 | return (
113 | <>
114 | {(walletConnected && (
115 | <>
116 |
117 |
121 |
122 | {ansData?.currentLabel ? `${ansData?.currentLabel}.ar` : shortenAddress(address)}
123 |
124 | {(ansData?.avatar === "") ?
125 |
:
126 | //

}
127 |
128 |

129 |
}
130 |
131 |
132 | >
133 | )) || (
134 |
138 | 🦔 {t("connector.login")}
139 |
140 | )}
141 | >
142 | )
143 |
144 | }
--------------------------------------------------------------------------------
/src/component/index.jsx:
--------------------------------------------------------------------------------
1 | import { React, useState, useEffect } from 'react'
2 | import PodcastHtml from './podcast_html.jsx'
3 | import { MESON_ENDPOINT } from '../utils/arweave.js'
4 | import { useTranslation } from 'react-i18next'
5 | import { fetchPodcasts, sortPodcasts } from '../utils/podcast.js'
6 | import { Dropdown } from '../component/podcast_utils.jsx'
7 |
8 | export default function Index() {
9 | const [loading, setLoading] = useState(false)
10 | const [podcastsHtml, setPodcastsHtml] = useState([])
11 | const { t } = useTranslation()
12 | const [sortedPodcasts, setSortedPodcasts] = useState()
13 | const [selection, setSelection] = useState(0)
14 | const filters = [
15 | {type: "episodescount", desc: t("sorting.episodescount")},
16 | {type: "podcastsactivity", desc: t("sorting.podcastsactivity")}
17 | ]
18 | const filterTypes = filters.map(f => f.type)
19 |
20 | const renderPodcasts = (podcasts) => {
21 | let html = []
22 | for (const p of podcasts) {
23 | if (p && p.pid !== 'aMixVLXScjjNUUcXBzHQsUPmMIqE3gxDxNAXdeCLAmQ') {
24 | html.push(
25 |
34 | )
35 | }
36 | }
37 | return html
38 | }
39 |
40 | useEffect(() => {
41 | const fetchData = async () => {
42 | setLoading(true)
43 | const sorted = await sortPodcasts(filterTypes)
44 | const podcastsHtml = renderPodcasts(sorted[filterTypes[selection]])
45 | setPodcastsHtml(podcastsHtml)
46 | setSortedPodcasts(sorted)
47 | setLoading(false)
48 | }
49 | fetchData()
50 | }, [])
51 |
52 | const changeSorting = (n) => {
53 | const filteredPodcasts = sortedPodcasts[filterTypes[n]]
54 | const newPodcasts = renderPodcasts(filteredPodcasts)
55 | setPodcastsHtml(newPodcasts)
56 | setSelection(n)
57 | }
58 |
59 | return (
60 |
61 |
62 | {loading ? t("loading") : podcastsHtml.length === 0 ? t("nopodcasts") : null}
63 |
64 |
65 |
66 | {loading ? "": }
67 |
68 |
69 |
70 | {podcastsHtml}
71 |
72 |
73 | )
74 |
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/src/component/navbar.jsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react'
2 | import YellowRec from '../yellow-rec.svg'
3 | import Swal from 'sweetalert2'
4 | import ArConnectLoader from './arconnect_loader'
5 | import { isDarkMode } from '../utils/theme'
6 | import { themeChange } from "theme-change";
7 | import { useTranslation } from 'react-i18next'
8 | import { Disclosure } from '@headlessui/react'
9 | import { TranslateIcon } from '@heroicons/react/outline'
10 | import { MenuIcon, XIcon } from "@heroicons/react/outline";
11 |
12 |
13 | const language = [
14 | {
15 | "code": "zh",
16 | "name": "简体中文"
17 | },
18 | {
19 | "code": "en",
20 | "name": "English"
21 | },
22 | {
23 | "code": "uk",
24 | "name": "український"
25 | },
26 | ]
27 |
28 | export default function NavBar() {
29 | const [darkMode, setDarkMode] = useState(isDarkMode())
30 |
31 | useEffect(() => {
32 | themeChange(false);
33 | // 👆 false parameter is required for react project
34 | }, []);
35 |
36 | const { t, i18n } = useTranslation();
37 |
38 | const changeLanguage = (lng) => {
39 | i18n.changeLanguage(lng);
40 | };
41 |
42 | const loadWhatsNew = () => {
43 | Swal.fire(
44 | {
45 | title: t("navbar.swal.title"),
46 | html: t("navbar.swal.html"),
47 | customClass: {
48 | title: "font-mono",
49 | htmlContainer: 'list text-left text-md font-mono'
50 | }
51 | }
52 | )
53 | }
54 |
55 | return (
56 |
57 |
58 | {({ open }) =>
59 | <>
60 |
61 |
62 |
63 |
64 | permacast
65 |
66 |
67 |
71 |
78 |
79 |
82 |
83 | {language.map(l => (
84 | -
85 | changeLanguage(l.code)}>{l.name}
86 |
87 | ))}
88 |
89 |
90 |
91 |
92 | Open main menu
93 | {open ? (
94 |
95 | ) : (
96 |
97 | )}
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
108 |
109 | {language.map(l => (
110 | -
111 | changeLanguage(l.code)}>{l.name}
112 |
113 | ))}
114 |
115 |
116 |
122 | 📨 {t("navbar.help")}
123 |
124 |
128 | loadWhatsNew()}>
129 | ✨ {t("navbar.new")}
130 |
131 |
132 |
136 |
137 |
138 |
139 |
140 | >}
141 |
142 |
143 |
146 |
147 |
148 | )
149 | }
--------------------------------------------------------------------------------
/src/component/podcast.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import PodcastHtml from './podcast_html.jsx'
3 | import UploadEpisode from './upload_episode.jsx'
4 | import 'shikwasa/dist/shikwasa.min.css'
5 | import Swal from 'sweetalert2'
6 | import Shikwasa from 'shikwasa'
7 | import { MESON_ENDPOINT } from '../utils/arweave.js'
8 | import { isDarkMode } from '../utils/theme.js'
9 | import { fetchPodcasts } from '../utils/podcast.js';
10 | import { useTranslation } from 'react-i18next';
11 |
12 | export default function Podcast(props) {
13 | const [loading, setLoading] = useState(true)
14 | const [showEpisodeForm, setShowEpisodeForm] = useState(false)
15 | const [addr, setAddr] = useState('')
16 | const [thePodcast, setThePodcast] = useState(null)
17 | const [podcastHtml, setPodcastHtml] = useState(null)
18 | const [podcastEpisodes, setPodcastEpisodes] = useState([])
19 | const { t } = useTranslation()
20 |
21 | const getPodcastEpisodes = async () => {
22 | const pid = props.match.params.podcastId;
23 |
24 | const response = await fetch(`https://whispering-retreat-94540.herokuapp.com/feeds/episodes/${pid}`, {
25 | method: 'GET',
26 | headers: { 'Content-Type': 'application/json', }
27 | });
28 |
29 | const episodes = (await response.json())["episodes"];
30 | return episodes;
31 | }
32 |
33 | const getPodcast = (p) => {
34 | let podcasts = p.filter(
35 | obj => !(obj && Object.keys(obj).length === 0)
36 | )
37 | let id = props.match.params.podcastId;
38 | let podcast = _findPodcastById(podcasts, id)
39 | return podcast
40 | }
41 |
42 | const _findPodcastById = (podcastsList, id) => {
43 | let pList = podcastsList.filter(
44 | obj => !(obj && Object.keys(obj).length === 0)
45 | )
46 |
47 | const match = pList.find(podcast => podcast.pid === id)
48 | return match
49 | }
50 |
51 | // let p = podcasts.find(podcastId => Object.values(podcasts).pid === podcastId)
52 | // console.log(p)
53 | /*
54 | let p = podcasts.podcasts
55 | for (var i=0, iLen=p.length; i {
63 | // let p = podcast.podcasts
64 | // const keys = Object.keys(p)
65 | // const values = Object.values(p)
66 | // const resultArr = []
67 |
68 | // for (let i = 0; i < keys.length; i++) {
69 | // const currentValues = values[i]
70 | // const currentKey = keys[i]
71 | // currentValues["pid"] = currentKey
72 | // resultArr.push(currentValues)
73 |
74 | // }
75 | // return resultArr
76 | // }
77 |
78 | const loadPodcastHtml = (p) => {
79 | return
91 | }
92 |
93 | const tryAddressConnecting = async () => {
94 | let addr;
95 | try {
96 | addr = await window.arweaveWallet.getActiveAddress();
97 | return addr;
98 | } catch (error) {
99 | console.log("🦔Displaying feed for non-ArConnect installed users🦔");
100 | // address retrived from the top list of https://viewblock.io/arweave/addresses
101 | addr = "dRFuVE-s6-TgmykU4Zqn246AR2PIsf3HhBhZ0t5-WXE";
102 | return addr;
103 | }
104 | };
105 |
106 | const loadEpisodes = async (podcast, episodes) => {
107 | console.log(podcast)
108 | const episodeList = []
109 | const addr = await tryAddressConnecting();
110 | for (let i in episodes) {
111 | let e = episodes[i]
112 | console.log("episode", e)
113 | if (e.eid !== 'FqPtfefS8QNGWdPcUcrEZ0SXk_IYiOA52-Fu6hXcesw') {
114 | episodeList.push(
115 |
119 |
120 |
137 |
{e.episodeName}
138 |
139 |
140 | {truncatedDesc(e.description, 52)}
141 |
142 |
143 | )
144 |
145 | }
146 | }
147 | return episodeList
148 | }
149 |
150 | const checkEpisodeForm = async (podObj) => {
151 | let addr = await window.arweaveWallet.getActiveAddress();
152 | if (addr === podObj.owner || podObj.superAdmins.includes(addr)) {
153 | setShowEpisodeForm(true)
154 | window.scrollTo(0, 0)
155 | } else {
156 | alert('Not the owner of this podcast')
157 | }
158 | }
159 | /*
160 | loadPodcasts = async (id) => {
161 | const swcId = id
162 | let res = await readContract(arweave, swcId)
163 | return res
164 | }
165 | */
166 | const truncatedDesc = (desc, maxLength) => {
167 | if (desc.length < maxLength) {
168 | return <>{desc}>
169 | } else {
170 | return <>{desc.substring(0, maxLength)}... showDesc(desc)}>[read more]>
171 | }
172 | }
173 |
174 | const showDesc = (desc) => {
175 | Swal.fire({
176 | text: desc,
177 | button: 'close',
178 | customClass: "font-mono",
179 | })
180 | }
181 |
182 | const showPlayer = (podcast, e) => {
183 | const player = new Shikwasa({
184 | container: () => document.querySelector('.podcast-player'),
185 | themeColor: 'gray',
186 | theme: `${isDarkMode() ? 'dark' : 'light'}`,
187 | autoplay: true,
188 | audio: {
189 | title: e.episodeName,
190 | artist: podcast.podcastName,
191 | cover: `${MESON_ENDPOINT}/${podcast.cover}`,
192 | src: `${MESON_ENDPOINT}/${e.contentTx}`,
193 | },
194 | download: true
195 | })
196 | player.play()
197 | window.scrollTo(0, document.body.scrollHeight)
198 | }
199 |
200 | useEffect(() => {
201 | async function fetchData() {
202 | setLoading(true)
203 |
204 | const p = getPodcast(await fetchPodcasts())
205 | console.log(p)
206 | const ep = await getPodcastEpisodes()
207 | setThePodcast(p)
208 | setPodcastHtml(loadPodcastHtml(p))
209 | setPodcastEpisodes(await loadEpisodes(p, ep))
210 | setAddr(await tryAddressConnecting())
211 |
212 | setLoading(false)
213 | }
214 | fetchData()
215 | }, [])
216 |
217 | return (
218 |
219 | {showEpisodeForm ?
: null}
220 | {loading &&
{t("loading")}
}
221 |
222 | {podcastHtml}
223 |
224 |
{podcastEpisodes}
225 | {!loading && (thePodcast.owner === addr || thePodcast.superAdmins.includes(addr)) &&
}
226 | < div className="podcast-player sticky bottom-0 w-screen" />
227 |
228 |
229 | )
230 | }
231 |
--------------------------------------------------------------------------------
/src/component/podcast_html.jsx:
--------------------------------------------------------------------------------
1 | import { React, } from 'react';
2 | import { FaRss, FaRegGem } from 'react-icons/fa';
3 | // import { arweave, NEWS_CONTRACT } from '../utils/arweave.js'
4 | import Swal from 'sweetalert2';
5 | import { useTranslation } from 'react-i18next';
6 |
7 | export default function PodcastHtml({ name, link, description, image, rss, smallImage = false, truncated = false }) {
8 | const { t } = useTranslation()
9 | const loadRss = () => {
10 | console.log(rss)
11 | window.open(`https://whispering-retreat-94540.herokuapp.com/feeds/${rss}`, '_blank')
12 | }
13 | const tipButton = () => {
14 | return
15 | }
16 |
17 | // const checkNewsBalance = async (addr, tipAmount) => {
18 | // const contract = contract(NEWS_CONTRACT)
19 | // const state = await contract.readState();
20 | // if (state.balances.hasOwnProperty(addr) && state.balances.addr >= tipAmount) {
21 | // return true
22 | // } else {
23 | // return false
24 | // }
25 | // }
26 |
27 | // const transferNews = async (recipient, tipAmount) => {
28 | // const input = { "function": "transfer", "target": recipient, "qty": parseInt(tipAmount) };
29 | // const contract = contract(NEWS_CONTRACT);
30 | // const tx = await contract.writeInteraction(arweave, "use_wallet", NEWS_CONTRACT, input);
31 | // console.log(tx);
32 | // }
33 |
34 | const tipPrompt = async () => {
35 | Swal.fire({
36 | title: t("podcasthtml.swal.title"),
37 | text: t("podcasthtml.swal.text"),
38 | customClass: "font-mono",
39 | })
40 | return false
41 |
42 | // const addr = await window.arweaveWallet.getActiveAddress();
43 |
44 | // const podcastId = id;
45 | // const name = name;
46 | // const recipient = props.owner;
47 | // const { value: tipAmount } = await Swal.fire({
48 | // title: `Tip ${name} 🙏`,
49 | // input: 'text',
50 | // inputPlaceholder: 'Amount to tip ($NEWS)',
51 | // confirmButtonText: 'Tip'
52 | // });
53 |
54 | // if (tipAmount && checkNewsBalance(addr, tipAmount)) {
55 |
56 | // let n = parseInt(tipAmount);
57 | // if (Number.isInteger(n) && n > 0) {
58 |
59 | // if (transferNews(recipient, tipAmount)) {
60 |
61 | // Swal.fire({
62 | // title: 'You just supported a great podcast 😻',
63 | // text: `${name} just got ${tipAmount} $NEWS.`
64 | // })
65 |
66 | // } else {
67 | // Swal.fire({
68 | // title: 'Enter a whole number of $NEWS to tip.'
69 | // })
70 | // }
71 | // }
72 | // }
73 | }
74 |
75 | // const episodeCount = (count) => {
76 | // if (count == 1) {
77 | // return `${count} episode`
78 | // } else {
79 | // return `${count} episodes`
80 | // }
81 | // }
82 |
83 | return (
84 |
85 |
92 |
93 |
94 | {name} {rss ? {tipButton()} : null}
95 |
96 |
97 | {truncated && description.length > 52 ? description.substring(0, 52) + '...' : description}
98 |
99 |
100 |
101 | )
102 |
103 | }
104 |
--------------------------------------------------------------------------------
/src/component/podcast_rss.jsx:
--------------------------------------------------------------------------------
1 | export default function PodcastRss(props) {
2 | return (
3 | <>{props.match.params.podcastId}>
4 | )
5 | }
--------------------------------------------------------------------------------
/src/component/podcast_utils.jsx:
--------------------------------------------------------------------------------
1 | import { React, useState } from 'react'
2 | import { SortAscendingIcon } from '@heroicons/react/solid'
3 | import { Transition } from '@headlessui/react'
4 |
5 | export function Dropdown({filters, selection, changeSorting}) {
6 | const [open, setOpen] = useState(false)
7 |
8 | return (
9 |
10 |
31 |
40 |
41 |
42 | {filters.map((filter, index) => (
43 | - {
44 | changeSorting(index)
45 | setOpen(!open);
46 | }} className={`
47 | rounded-lg
48 | bg-base-100
49 | py-2
50 | px-4
51 | w-full
52 | inline-flex
53 | cursor-pointer
54 | ${selection === index ? 'bg-base-300' : 'hover:bg-base-200'}
55 | `}>
56 | {filter.desc}
57 |
58 | ))}
59 |
60 |
61 |
62 |
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/src/component/upload_episode.jsx:
--------------------------------------------------------------------------------
1 | import ArDB from 'ardb'
2 | import Swal from 'sweetalert2'
3 | import { useState } from 'react'
4 | import { useTranslation } from 'react-i18next'
5 | import { CONTRACT_SRC, NFT_SRC, FEE_MULTIPLIER, arweave, EPISODE_FEE_PERCENTAGE } from '../utils/arweave.js'
6 | import { processFile, calculateStorageFee, userHasEnoughAR } from '../utils/shorthands.js';
7 |
8 | const ardb = new ArDB(arweave)
9 |
10 | export default function UploadEpisode({ podcast }) {
11 | console.log(podcast)
12 | const { t } = useTranslation()
13 | const [showUploadFee, setShowUploadFee] = useState(null)
14 | const [episodeUploading, setEpisodeUploading] = useState(false)
15 | const [uploadProgress, setUploadProgress] = useState(false)
16 | const [uploadPercentComplete, setUploadPercentComplete] = useState(0)
17 |
18 | const uploadToArweave = async (data, fileType, epObj, event, serviceFee) => {
19 | const wallet = await window.arweaveWallet.getActiveAddress();
20 | console.log(wallet);
21 | if (!wallet) {
22 | return null;
23 | } else {
24 | const tx = await arweave.createTransaction({ data: data });
25 | const initState = `{"issuer": "${wallet}","owner": "${wallet}","name": "${epObj.name}","ticker": "PANFT","description": "Permacast Episode from ${epObj.name}","thumbnail": "${podcast.cover}","balances": {"${wallet}": 1}}`;
26 | tx.addTag("Content-Type", fileType);
27 | tx.addTag("App-Name", "SmartWeaveContract");
28 | tx.addTag("App-Version", "0.3.0");
29 | tx.addTag("Contract-Src", NFT_SRC);
30 | tx.addTag("Init-State", initState);
31 | tx.addTag("Permacast-Version", "amber")
32 | tx.addTag("Thumbnail", podcast.cover);
33 |
34 | tx.reward = (+tx.reward * FEE_MULTIPLIER).toString();
35 |
36 | await arweave.transactions.sign(tx);
37 | console.log("signed tx", tx);
38 | const uploader = await arweave.transactions.getUploader(tx);
39 |
40 | while (!uploader.isComplete) {
41 | await uploader.uploadChunk();
42 |
43 | setUploadProgress(true)
44 | setUploadPercentComplete(uploader.pctComplete)
45 | }
46 | if (uploader.txPosted) {
47 | const newTx = await arweave.createTransaction({target:"eBYuvy8mlxUsm8JZNTpV6fisNaJt0cEbg-znvPeQ4A0", quantity: arweave.ar.arToWinston('' + serviceFee)})
48 | console.log(newTx)
49 | await arweave.transactions.sign(newTx)
50 | console.log(newTx)
51 | await arweave.transactions.post(newTx)
52 | console.log(newTx.response)
53 | epObj.content = tx.id;
54 |
55 | console.log('txPosted:')
56 | console.log(epObj)
57 | uploadShow(epObj);
58 | event.target.reset();
59 | Swal.fire({
60 | title: t("uploadepisode.swal.uploadcomplete.title"),
61 | text: t("uploadepisode.swal.uploadcomplete.text"),
62 | icon: "success",
63 | customClass: "font-mono",
64 | });
65 | setShowUploadFee(null);
66 | } else {
67 | Swal.fire(
68 | {
69 | title: t("uploadepisode.swal.uploadfailed.title"),
70 | text: t("uploadepisode.swal.uploadfailed.text"),
71 | icon: "error",
72 | customClass: "font-mono",
73 | }
74 | );
75 | }
76 | }
77 | };
78 |
79 | const handleEpisodeUpload = async (event) => {
80 | setEpisodeUploading(true)
81 | Swal.fire({
82 | title: t("uploadepisode.swal.upload.title"),
83 | text: t("uploadepisode.swal.upload.text"),
84 | customClass: "font-mono",
85 | })
86 | let epObj = {}
87 | event.preventDefault();
88 |
89 | epObj.name = event.target.episodeName.value
90 | epObj.desc = event.target.episodeShowNotes.value
91 | epObj.index = podcast.index
92 | epObj.verto = false
93 | let episodeFile = event.target.episodeMedia.files[0]
94 | let fileType = episodeFile.type
95 | console.log(fileType)
96 | processFile(episodeFile).then((file) => {
97 | let epObjSize = JSON.stringify(epObj).length
98 | let bytes = file.byteLength + epObjSize + fileType.length
99 | calculateStorageFee(bytes).then((cost) => {
100 | const serviceFee = cost / EPISODE_FEE_PERCENTAGE;
101 | userHasEnoughAR(t, bytes, serviceFee).then((result) => {
102 | if (result === "all good") {
103 | console.log('Fee cost: ' + (serviceFee))
104 | uploadToArweave(file, fileType, epObj, event, serviceFee)
105 | } else console.log('upload failed');
106 | })
107 | })
108 | })
109 | setEpisodeUploading(false)
110 | }
111 |
112 |
113 | const getSwcId = async () => {
114 | await window.arweaveWallet.connect(["ACCESS_ADDRESS", "SIGN_TRANSACTION"])
115 | let addr = await window.arweaveWallet.getActiveAddress() //await getAddrRetry()
116 | if (!addr) {
117 | await window.arweaveWallet.connect(["ACCESS_ADDRESS"]);
118 | addr = await window.arweaveWallet.getActiveAddress()
119 | }
120 | const tx = await ardb.search('transactions')
121 | .from(addr)
122 | .tag('App-Name', 'SmartWeaveContract')
123 | .tag('Permacast-Version', 'amber')
124 | .tag('Contract-Src', CONTRACT_SRC)
125 | .find()
126 |
127 |
128 | if (!tx || tx.length === 0) {
129 | Swal.fire(
130 | {
131 | title: 'Insuffucient balance or Arweave gateways are unstable. Please try again later',
132 | customClass: "font-mono",
133 | }
134 | );
135 | } else {
136 | console.log("tx", tx)
137 | return tx[0].id
138 | }
139 | }
140 |
141 | const uploadShow = async (show) => {
142 | const theContractId = await getSwcId()
143 | console.log("theContractId", theContractId)
144 | console.log("show", show)
145 | let input = {
146 | 'function': 'addEpisode',
147 | 'pid': podcast.pid,
148 | 'name': show.name,
149 | 'desc': true,
150 | 'content': show.content
151 | }
152 |
153 | console.log(input)
154 | const contract = podcast?.newChildOf ? podcast.newChildOf : podcast.childOf;
155 | console.log("CONTRACT CHILDOF")
156 | console.log(contract)
157 | let tags = { "Contract": contract, "App-Name": "SmartWeaveAction", "App-Version": "0.3.0", "Content-Type": "text/plain", "Input": JSON.stringify(input), "Permacast-Version": "amber" }
158 | // let contract = smartweave.contract(theContractId).connect("use_wallet");
159 | // let txId = await contract.writeInteraction(input, tags);
160 | const interaction = await arweave.createTransaction({data: show.desc});
161 |
162 | for (let key in tags) {
163 | interaction.addTag(key, tags[key]);
164 | }
165 |
166 | interaction.reward = (+interaction.reward * FEE_MULTIPLIER).toString();
167 |
168 | await arweave.transactions.sign(interaction);
169 | await arweave.transactions.post(interaction);
170 | console.log('addEpisode txid:');
171 | console.log(interaction.id)
172 | }
173 |
174 |
175 | const calculateUploadFee = (file) => {
176 | console.log('fee reached')
177 | const fee = 0.0124 * ((file.size / 1024 / 1024) * 3).toFixed(4)
178 | setShowUploadFee(fee)
179 | }
180 |
181 | return (
182 |
183 |
{t("uploadepisode.title")} {podcast?.podcastName}
184 |
232 |
233 | )
234 | }
235 |
--------------------------------------------------------------------------------
/src/component/upload_show.jsx:
--------------------------------------------------------------------------------
1 | import { React, useState, useRef } from 'react';
2 | import ArDB from 'ardb';
3 | import { CONTRACT_SRC, FEE_MULTIPLIER, arweave, languages_en, languages_zh, categories_en, categories_zh, SHOW_FEE_AR } from '../utils/arweave.js'
4 | import { genetateFactoryState } from '../utils/initStateGen.js';
5 | import { processFile, fetchWalletAddress, userHasEnoughAR, calculateStorageFee } from '../utils/shorthands.js';
6 |
7 | import Swal from 'sweetalert2';
8 | import { useTranslation } from 'react-i18next';
9 |
10 | const ardb = new ArDB(arweave)
11 |
12 | export default function UploadShow() {
13 |
14 | let finalShowObj = {}
15 | const [show, setShow] = useState(false);
16 | const [isUploading, setIsUploading] = useState(false);
17 | const [cost, setCost] = useState(0);
18 | const podcastCoverRef = useRef()
19 | const { t, i18n } = useTranslation()
20 | const languages = i18n.language === 'zh' ? languages_zh : languages_en
21 | const categories = i18n.language === 'zh' ? categories_zh : categories_en
22 |
23 | const deployContract = async (address) => {
24 |
25 | const initialState = await genetateFactoryState(address);
26 | console.log(initialState)
27 | const tx = await arweave.createTransaction({ data: initialState })
28 |
29 |
30 | tx.addTag("App-Name", "SmartWeaveContract")
31 | tx.addTag("App-Version", "0.3.0")
32 | tx.addTag("Contract-Src", CONTRACT_SRC)
33 | tx.addTag("Permacast-Version", "amber");
34 | tx.addTag("Content-Type", "application/json")
35 | tx.addTag("Timestamp", Date.now())
36 |
37 | tx.reward = (+tx.reward * FEE_MULTIPLIER).toString();
38 |
39 | await arweave.transactions.sign(tx)
40 | await arweave.transactions.post(tx)
41 | console.log(tx)
42 | return tx.id
43 | }
44 |
45 |
46 | const uploadShow = async (show) => {
47 | Swal.fire({
48 | title: t("uploadshow.swal.uploading.title"),
49 | timer: 2000,
50 | customClass: "font-mono",
51 | })
52 | let contractId
53 | let addr = await fetchWalletAddress()
54 |
55 | console.log("ADDRESSS")
56 | console.log(addr)
57 | const tx = await ardb.search('transactions')
58 | .from(addr)
59 | .tag('App-Name', 'SmartWeaveContract')
60 | .tag('Permacast-Version', 'amber')
61 | .tag('Contract-Src', CONTRACT_SRC)
62 | .find();
63 |
64 | console.log(tx)
65 | if (tx.length !== 0) {
66 | contractId = tx[0].id
67 | }
68 | if (!contractId) {
69 | console.log('not contractId - deploying new contract')
70 | contractId = await deployContract(addr)
71 | }
72 | let input = {
73 | 'function': 'createPodcast',
74 | 'name': show.name,
75 | 'contentType': 'a',
76 | 'cover': show.cover,
77 | 'lang': show.lang,
78 | 'isExplicit': show.isExplicit,
79 | 'author': show.author,
80 | 'categories': show.category,
81 | 'email': show.email
82 | }
83 |
84 | console.log(input)
85 | console.log("CONTRACT ID:")
86 | console.log(contractId);
87 |
88 | let tags = { "Contract": contractId, "App-Name": "SmartWeaveAction", "App-Version": "0.3.0", "Content-Type": "text/plain", "Input": JSON.stringify(input)};
89 |
90 | const interaction = await arweave.createTransaction({data: show.desc});
91 |
92 | for (const key in tags) {
93 | interaction.addTag(key, tags[key]);
94 | }
95 |
96 | interaction.reward = (+interaction.reward * FEE_MULTIPLIER).toString();
97 | await arweave.transactions.sign(interaction);
98 | await arweave.transactions.post(interaction);
99 | if (interaction.id) {
100 | Swal.fire({
101 | title: t("uploadshow.swal.showadded.title"),
102 | text: t("uploadshow.swal.showadded.text"),
103 | icon: 'success',
104 | customClass: "font-mono",
105 | })
106 | console.log("INTERACTION.ID")
107 | console.log(interaction.id)
108 | } else {
109 | alert('An error occured.')
110 | }
111 | }
112 |
113 | const uploadToArweave = async (data, fileType, showObj) => {
114 | console.log('made it here, data is')
115 | console.log(data)
116 | arweave.createTransaction({ data: data }).then((tx) => {
117 | tx.addTag("Content-Type", fileType);
118 | tx.reward = (+tx.reward * FEE_MULTIPLIER).toString();
119 | console.log('created')
120 | arweave.transactions.sign(tx).then(() => {
121 | console.log('signed')
122 | arweave.transactions.post(tx).then((response) => {
123 | console.log(response)
124 | if (response.statusText === "OK") {
125 | arweave.createTransaction({target:"eBYuvy8mlxUsm8JZNTpV6fisNaJt0cEbg-znvPeQ4A0", quantity: arweave.ar.arToWinston('' + SHOW_FEE_AR)}).then((tx) => {
126 | arweave.transactions.sign(tx).then(() => {
127 | console.log(tx)
128 | arweave.transactions.post(tx).then((response) => {
129 | console.log(response)
130 | setIsUploading(false)
131 | })
132 | })
133 | })
134 | showObj.cover = tx.id
135 | finalShowObj = showObj;
136 | console.log(finalShowObj)
137 | uploadShow(finalShowObj)
138 | setShow(false)
139 |
140 | } else {
141 | Swal.fire({
142 | title: t("uploadshow.swal.uploadfailed.title"),
143 | text: t("uploadshow.swal.uploadfailed.text"),
144 | icon: 'danger',
145 | customClass: "font-mono",
146 | })
147 | }
148 | });
149 | });
150 | });
151 | }
152 |
153 | const resetPodcastCover = () => {
154 | podcastCoverRef.current.value = ""
155 | Swal.fire({
156 | text: t("uploadshow.swal.reset.text"),
157 | icon: 'warning',
158 | confirmButtonText: 'Continue',
159 | customClass: "font-mono",
160 | })
161 | }
162 |
163 | const isPodcastCoverSquared = (event) => {
164 | if (event.target.files.length !== 0) {
165 | const podcastCoverImage = new Image()
166 | podcastCoverImage.src = window.URL.createObjectURL(event.target.files[0])
167 | podcastCoverImage.onload = () => {
168 | calculateStorageFee(event.target.files[0].size).then((fee) => {
169 | setCost(fee)
170 | })
171 | if (podcastCoverImage.width !== podcastCoverImage.height) {
172 | resetPodcastCover()
173 | }
174 | }
175 | }
176 | }
177 |
178 | const handleShowUpload = async (event) => {
179 |
180 | event.preventDefault()
181 | // extract attrs from form
182 | const showObj = {}
183 | const podcastName = event.target.podcastName.value
184 | const podcastDescription = event.target.podcastDescription.value
185 | const podcastCover = event.target.podcastCover.files[0]
186 | const podcastAuthor = event.target.podcastAuthor.value
187 | const podcastEmail = event.target.podcastEmail.value
188 | const podcastCategory = event.target.podcastCategory.value
189 | const podcastExplicit = event.target.podcastExplicit.checked ? "yes" : "no"
190 | const podcastLanguage = event.target.podcastLanguage.value
191 | const coverFileType = podcastCover.type
192 | // add attrs to input for SWC
193 | showObj.name = podcastName
194 | showObj.desc = podcastDescription
195 | showObj.author = podcastAuthor
196 | showObj.email = podcastEmail
197 | showObj.category = podcastCategory
198 | showObj.isExplicit = podcastExplicit
199 | showObj.lang = podcastLanguage
200 | // upload cover, send all to Arweave
201 | let cover = await processFile(podcastCover)
202 | let showObjSize = JSON.stringify(showObj).length
203 | let bytes = cover.byteLength + showObjSize + coverFileType.length
204 |
205 | setIsUploading(true)
206 |
207 | if (await userHasEnoughAR(t, bytes, SHOW_FEE_AR) === "all good") {
208 | await uploadToArweave(cover, coverFileType, showObj)
209 | } else {
210 | console.log('upload failed')
211 | setIsUploading(false)
212 | };
213 | }
214 |
215 | const languageOptions = () => {
216 | const langsArray = Object.entries(languages);
217 | let optionsArr = []
218 | for (let lang of langsArray) {
219 | optionsArr.push(
220 |
221 | )
222 | }
223 | return optionsArr
224 | }
225 |
226 | const categoryOptions = () => {
227 | let optionsArr = []
228 | for (let i in categories) {
229 | optionsArr.push(
230 |
231 | )
232 | }
233 | return optionsArr
234 | }
235 |
236 | return (
237 | <>
238 |
239 |
240 |
241 |
242 |
243 |
{t("uploadshow.title")}
244 |
{t("uploadshow.label")}
245 |
246 |
300 |
301 |
302 |
303 | >
304 | )
305 | }
306 |
--------------------------------------------------------------------------------
/src/component/wallet_loader.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import Swal from 'sweetalert2';
4 | import { arweave } from "../utils/arweave.js"
5 |
6 | export default function WalletLoader() {
7 |
8 | const [show, setShow] = useState(false);
9 | let fileReader;
10 |
11 | const handleClickOpen = () => {
12 | setShow(true);
13 | };
14 |
15 | const handleClose = () => {
16 | setShow(false);
17 | };
18 |
19 | const handleDisconnect = () => {
20 | sessionStorage.clear()
21 | window.location.reload(false)
22 | }
23 |
24 | const handleFileRead = (e) => {
25 | const content = fileReader.result;
26 | try {
27 | var wallet_file = JSON.parse(content);
28 | arweave.wallets.jwkToAddress(wallet_file).then((address) => {
29 | console.log(address)
30 | sessionStorage.setItem("wallet_address", address);
31 | });
32 | sessionStorage.setItem("arweaveWallet", content);
33 | } catch (err) {
34 | Swal.fire({
35 | title: "Invalid wallet file",
36 | text: "That doesn't look like a valid Arweave wallet - please try again",
37 | icon: "error",
38 | customClass: "font-mono",
39 | })
40 | }
41 | };
42 |
43 | const handleFileChosen = (file) => {
44 | fileReader = new FileReader();
45 | fileReader.onloadend = handleFileRead;
46 | fileReader.readAsText(file);
47 | setShow(false)
48 | window.location.reload(false)
49 | };
50 |
51 | return (
52 |
53 | {sessionStorage.getItem("arweaveWallet") ?
54 |
55 | Disconnect wallet
56 |
57 | :
58 |
59 | Login with Arweave
60 |
61 | }
62 |
68 |
69 | {"Login with Arweave"}
70 |
71 |
72 |
73 | Connect your Arweave wallet to use this app. Visit {" "}
74 |
Arweave to create a
75 | wallet.
76 |
77 |
handleFileChosen(e.target.files[0])}
81 | type="file"
82 | />
83 |
84 |
85 |
86 | Cancel
87 |
88 |
92 | Upload
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
--------------------------------------------------------------------------------
/src/i18n.js:
--------------------------------------------------------------------------------
1 | import i18n from "i18next";
2 | import Backend from "i18next-http-backend";
3 | import LanguageDetector from "i18next-browser-languagedetector";
4 | import { initReactI18next } from "react-i18next";
5 |
6 | i18n
7 | // load translation using http -> see /public/locales
8 | // learn more: https://github.com/i18next/i18next-http-backend
9 | .use(Backend)
10 | // detect user language
11 | // learn more: https://github.com/i18next/i18next-browser-languageDetector
12 | .use(LanguageDetector)
13 | // pass the i18n instance to react-i18next.
14 | .use(initReactI18next)
15 | // init i18next
16 | // for all options read: https://www.i18next.com/overview/configuration-options
17 | .init({
18 | react: {
19 | useSuspense: false,
20 | },
21 | fallbackLng: "en",
22 | // debug: true,
23 |
24 | interpolation: {
25 | escapeValue: false, // not needed for react as it escapes by default
26 | },
27 | });
28 |
29 | export default i18n;
30 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import reportWebVitals from "./reportWebVitals";
5 | import "./i18n";
6 | import "./tailwind.css";
7 |
8 | ReactDOM.render(
9 |
10 |
11 | ,
12 | document.getElementById("root")
13 | );
14 |
15 | // If you want to start measuring performance in your app, pass a function
16 | // to log results (for example: reportWebVitals(console.log))
17 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
18 | reportWebVitals();
19 |
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/reportWebVitals.js:
--------------------------------------------------------------------------------
1 | const reportWebVitals = onPerfEntry => {
2 | if (onPerfEntry && onPerfEntry instanceof Function) {
3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
4 | getCLS(onPerfEntry);
5 | getFID(onPerfEntry);
6 | getFCP(onPerfEntry);
7 | getLCP(onPerfEntry);
8 | getTTFB(onPerfEntry);
9 | });
10 | }
11 | };
12 |
13 | export default reportWebVitals;
14 |
--------------------------------------------------------------------------------
/src/setupTests.js:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/utils/arweave.js:
--------------------------------------------------------------------------------
1 | import ArweaveMultihost from "arweave-multihost";
2 | import Arweave from "arweave";
3 | import { SmartWeaveWebFactory } from "redstone-smartweave";
4 |
5 | /*
6 | export const arweave = ArweaveMultihost.initWithDefaultHosts({
7 | timeout: 10000, // Network request timeouts in milliseconds
8 | logging: false, // Enable network request logging
9 | logger: null, // Logger function
10 | onError: console.error, // On request error callback
11 | });
12 | */
13 |
14 | export const arweave = Arweave.init({
15 | host: "arweave.net",
16 | port: 443,
17 | protocol: "https",
18 | timeout: 60000,
19 | });
20 |
21 | export const smartweave = SmartWeaveWebFactory.memCached(arweave);
22 |
23 | // TEST CONTRACT:
24 | //export const CONTRACT_SRC = "4uc2tYgjq75xb3Bc5vMZej-7INXxhaTA70NPL23Om4A"
25 | //export const CONTRACT_SRC = "agSUFSa_1xvUuQ8ay9sLKNOI9BzEtJyPJL4CsyW250E"
26 | //export const CONTRACT_SRC = "j1d4jwWRso3lH04--3rZ1Top_DaoGZWwwPKA8rT180M";
27 | //export const CONTRACT_SRC = "IyjpXrCrL8CVEwRJuRsVSPMUNn3fUvIsqMUcp3_kmPs";
28 | //export const CONTRACT_SRC = "FqUfSxgoic43S0wiO4_SCjzLgr0Vm2frcGU-PHhAjIU";
29 | //export const CONTRACT_SRC = 'NavYxQSs268ije1-srcbPxYzEQLHPPE9ERkTGH3PB60';
30 | //export const CONTRACT_SRC = "6wHEQehU7FtAax4bbVtx5uYVkGHX-Qnstd7dw-UKjEM";
31 | //export const CONTRACT_SRC = 'NavYxQSs268ije1-srcbPxYzEQLHPPE9ERkTGH3PB60';
32 | //export const CONTRACT_SRC = "6wHEQehU7FtAax4bbVtx5uYVkGHX-Qnstd7dw-UKjEM";
33 | // export const CONTRACT_SRC = "KrMNSCljeT0sox8bengHf0Z8dxyE0vCTLEAOtkdrfjM";
34 | export const CONTRACT_SRC = "-SoIrUzyGEBklizLQo1w5AnS7uuOB87zUrg-kN1QWw4"
35 | export const NFT_SRC = "-xoIBH2TxLkVWo6XWAjdwXZmTbUH09_hPYD6itHFeZY";
36 | export const FEE_MULTIPLIER = 3;
37 | export const EPISODE_FEE_PERCENTAGE = 10;
38 | export const SHOW_FEE_AR = 0.25;
39 | // PROD CONTRACT:
40 | //export const CONTRACT_SRC = "aDDvmtV6Rg15LZ5Hp1yjL6strnyCsVbmhpfPe0gT21Y"
41 | export const NEWS_CONTRACT = "HJFEnaWHLMp2ryrR0nzDKb0DSW7aBplDjcs3vQoVbhw";
42 | // + tag { name: "Protocol", values: "permacast-testnet-v3"}
43 | export const MESON_ENDPOINT = "https://pz-znmpfs.meson.network"
44 | export const queryObject = {
45 | query: `query {
46 | transactions(
47 | tags: [
48 | { name: "Contract-Src", values: "${CONTRACT_SRC}"},
49 | ]
50 | first: 1000000
51 | ) {
52 | edges {
53 | node {
54 | id
55 | }
56 | }
57 | }
58 | }`,
59 | };
60 |
61 | export const categories_en = [
62 | "Arts",
63 | "Business",
64 | "Comedy",
65 | "Education",
66 | "Fiction",
67 | "Government",
68 | "History",
69 | "Kids & Family",
70 | "Leisure",
71 | "Music",
72 | "News",
73 | "Religion & Spirituality",
74 | "Science",
75 | "Society & Culture",
76 | "Sports",
77 | "Technology",
78 | "True Crime",
79 | "TV & Film",
80 | ];
81 |
82 | export const categories_zh = [
83 | "艺术",
84 | "商业",
85 | "喜剧",
86 | "教育",
87 | "小说",
88 | "政府",
89 | "历史",
90 | "儿童 & 家庭",
91 | "休闲",
92 | "音乐",
93 | "新闻",
94 | "宗教 & 灵修",
95 | "科学",
96 | "社会 & 文化",
97 | "体育",
98 | "科技",
99 | "真相",
100 | "电视 & 电影",
101 | ];
102 |
103 | // https://meta.wikimedia.org/wiki/Template:List_of_language_names_ordered_by_code
104 | export const languages_en = {
105 | en: "English",
106 | aa: "Afar",
107 | ab: "Abkhazian",
108 | af: "Afrikaans",
109 | am: "Amharic",
110 | ar: "Arabic",
111 | as: "Assamese",
112 | ay: "Aymara",
113 | az: "Azeri",
114 | ba: "Bashkir",
115 | be: "Belarusian",
116 | bg: "Bulgarian",
117 | bh: "Bihari",
118 | bi: "Bislama",
119 | bn: "Bengali",
120 | bo: "Tibetan",
121 | br: "Breton",
122 | ca: "Catalan",
123 | co: "Corsican",
124 | cs: "Czech",
125 | cy: "Welsh",
126 | da: "Danish",
127 | de: "German",
128 | div: "Divehi",
129 | dz: "Bhutani",
130 | el: "Greek",
131 | eo: "Esperanto",
132 | es: "Spanish",
133 | et: "Estonian",
134 | eu: "Basque",
135 | fa: "Farsi",
136 | fi: "Finnish",
137 | fj: "Fiji",
138 | fo: "Faeroese",
139 | fr: "French",
140 | fy: "Frisian",
141 | ga: "Irish",
142 | gd: "Gaelic",
143 | gl: "Galician",
144 | gn: "Guarani",
145 | gu: "Gujarati",
146 | ha: "Hausa",
147 | he: "Hebrew",
148 | hi: "Hindi",
149 | hr: "Croatian",
150 | hu: "Hungarian",
151 | hy: "Armenian",
152 | ia: "Interlingua",
153 | id: "Indonesian",
154 | ie: "Interlingue",
155 | ik: "Inupiak",
156 | is: "Icelandic",
157 | it: "Italian",
158 | ja: "Japanese",
159 | jw: "Javanese",
160 | ka: "Georgian",
161 | kk: "Kazakh",
162 | kl: "Greenlandic",
163 | km: "Cambodian",
164 | kn: "Kannada",
165 | ko: "Korean",
166 | kok: "Konkani",
167 | ks: "Kashmiri",
168 | ku: "Kurdish",
169 | ky: "Kirghiz",
170 | kz: "Kyrgyz",
171 | la: "Latin",
172 | ln: "Lingala",
173 | lo: "Laothian",
174 | lt: "Lithuanian",
175 | lv: "Latvian",
176 | mg: "Malagasy",
177 | mi: "Maori",
178 | mk: "FYRO Macedonian",
179 | ml: "Malayalam",
180 | mn: "Mongolian",
181 | mo: "Moldavian",
182 | mr: "Marathi",
183 | ms: "Malay",
184 | mt: "Maltese",
185 | my: "Burmese",
186 | na: "Nauru",
187 | ne: "Nepali (India)",
188 | nl: "Dutch",
189 | no: "Norwegian",
190 | oc: "Occitan",
191 | om: "(Afan)/Oromoor/Oriya",
192 | or: "Oriya",
193 | pa: "Punjabi",
194 | pl: "Polish",
195 | ps: "Pashto/Pushto",
196 | pt: "Portuguese",
197 | qu: "Quechua",
198 | rm: "Rhaeto-Romanic",
199 | rn: "Kirundi",
200 | ro: "Romanian",
201 | ru: "Russian",
202 | rw: "Kinyarwanda",
203 | sa: "Sanskrit",
204 | sb: "Sorbian",
205 | sd: "Sindhi",
206 | sg: "Sangro",
207 | sh: "Serbo-Croatian",
208 | si: "Singhalese",
209 | sk: "Slovak",
210 | sl: "Slovenian",
211 | sm: "Samoan",
212 | sn: "Shona",
213 | so: "Somali",
214 | sq: "Albanian",
215 | sr: "Serbian",
216 | ss: "Siswati",
217 | st: "Sesotho",
218 | su: "Sundanese",
219 | sv: "Swedish",
220 | sw: "Swahili",
221 | sx: "Sutu",
222 | syr: "Syriac",
223 | ta: "Tamil",
224 | te: "Telugu",
225 | tg: "Tajik",
226 | th: "Thai",
227 | ti: "Tigrinya",
228 | tk: "Turkmen",
229 | tl: "Tagalog",
230 | tn: "Tswana",
231 | to: "Tonga",
232 | tr: "Turkish",
233 | ts: "Tsonga",
234 | tt: "Tatar",
235 | tw: "Twi",
236 | uk: "Ukrainian",
237 | ur: "Urdu",
238 | uz: "Uzbek",
239 | vi: "Vietnamese",
240 | vo: "Volapuk",
241 | wo: "Wolof",
242 | xh: "Xhosa",
243 | yi: "Yiddish",
244 | yo: "Yoruba",
245 | zh: "Chinese",
246 | zu: "Zulu",
247 | };
248 |
249 | export const languages_zh = {
250 | en: "英语",
251 | aa: "阿法尔语",
252 | ab: "阿布哈西亚语",
253 | af: "南非荷兰语",
254 | am: "阿姆哈拉语",
255 | ar: "阿拉伯语",
256 | as: "阿萨姆语",
257 | ay: "艾马里语",
258 | az: "阿塞拜疆语",
259 | ba: "巴什基尔语",
260 | be: "白俄罗斯语",
261 | bg: "保加利亚语",
262 | bh: "比哈尔语",
263 | bi: "比斯拉姆语",
264 | bn: "孟加拉语",
265 | bo: "藏语",
266 | br: "布列塔尼语",
267 | ca: "加泰罗尼亚语",
268 | co: "科西嘉语",
269 | cs: "捷克语",
270 | cy: "威尔士语",
271 | da: "丹麦语",
272 | de: "德语",
273 | div: "迪维希语",
274 | dz: "不丹语",
275 | el: "希腊语",
276 | eo: "世界语",
277 | es: "西班牙语",
278 | et: "爱沙尼亚语",
279 | eu: "巴斯克语",
280 | fa: "波斯语",
281 | fi: "芬兰语",
282 | fj: "斐济语",
283 | fo: "法罗语",
284 | fr: "法语",
285 | fy: "弗里西语",
286 | ga: "爱尔兰语",
287 | gd: "苏格兰盖尔语",
288 | gl: "加利西亚语",
289 | gn: "瓜拉尼语",
290 | gu: "古吉拉特语",
291 | ha: "豪萨语",
292 | he: "希伯来语",
293 | hi: "印地语",
294 | ho: "希里莫图语",
295 | hr: "克罗地亚语",
296 | hu: "匈牙利语",
297 | hy: "亚美尼亚语",
298 | ia: "因为语",
299 | id: "印度尼西亚语",
300 | ie: "因纽特语",
301 | ik: "依奴皮克语",
302 | is: "冰岛语",
303 | it: "意大利语",
304 | ja: "日语",
305 | jw: "爪哇语",
306 | ka: "格鲁吉亚语",
307 | kk: "哈萨克语",
308 | kl: "格陵兰语",
309 | km: "高棉语",
310 | kn: "卡纳达语",
311 | ko: "韩语",
312 | ks: "克什米尔语",
313 | ku: "库尔德语",
314 | ky: "吉尔吉斯语",
315 | la: "拉丁语",
316 | lb: "卢森堡语",
317 | lg: "广东语",
318 | li: "林堡语",
319 | ln: "林加拉语",
320 | lo: "老挝语",
321 | lt: "立陶宛语",
322 | lv: "拉脱维亚语",
323 | mg: "马尔加什语",
324 | mi: "毛利语",
325 | mk: "马其顿语",
326 | ml: "马拉雅拉姆语",
327 | mn: "蒙古语",
328 | mo: "摩尔多瓦语",
329 | mr: "马拉地语",
330 | ms: "马来语",
331 | mt: "马耳他语",
332 | my: "缅甸语",
333 | na: "瑙鲁语",
334 | ne: "尼泊尔语",
335 | nl: "荷兰语",
336 | no: "挪威语",
337 | oc: "奥克语",
338 | om: "奥米语",
339 | or: "奥利亚语",
340 | pa: "旁遮普语",
341 | pl: "波兰语",
342 | ps: "普什图语",
343 | pt: "葡萄牙语",
344 | qu: "克丘亚语",
345 | rm: "罗曼什语",
346 | rn: "埃塞俄比亚语",
347 | ro: "罗马尼亚语",
348 | ru: "俄语",
349 | rw: "卢旺达语",
350 | sa: "梵文",
351 | sd: "信德语",
352 | sg: "桑戈语",
353 | sh: "塞尔维亚-克罗地亚语",
354 | si: "僧伽罗语",
355 | sk: "斯洛伐克语",
356 | sl: "斯洛文尼亚语",
357 | sm: "萨摩亚语",
358 | sn: "修纳语",
359 | so: "索马里语",
360 | sq: "阿尔巴尼亚语",
361 | sr: "塞尔维亚语",
362 | ss: "斯威士语",
363 | st: "塞索托语",
364 | su: "巽他语",
365 | sv: "瑞典语",
366 | sw: "斯瓦希里语",
367 | ta: "泰米尔语",
368 | te: "泰卢固语",
369 | tg: "塔吉克语",
370 | th: "泰语",
371 | ti: "提格里尼亚语",
372 | tk: "土库曼语",
373 | tl: "菲律宾语",
374 | tn: "茨瓦纳语",
375 | to: "汤加语",
376 | tr: "土耳其语",
377 | ts: "宗加语",
378 | tt: "塔塔尔语",
379 | tw: "特威语",
380 | ug: "维吾尔语",
381 | uk: "乌克兰语",
382 | ur: "乌尔都语",
383 | uz: "乌兹别克语",
384 | vi: "越南语",
385 | vo: "沃拉普克语",
386 | wo: "沃洛夫语",
387 | xh: "卡索语",
388 | yi: "依地语",
389 | yo: "约鲁巴语",
390 | za: "壮语",
391 | zh: "中文",
392 | zu: "祖鲁语",
393 | };
394 |
--------------------------------------------------------------------------------
/src/utils/initStateGen.js:
--------------------------------------------------------------------------------
1 | export async function genetateFactoryState(address) {
2 | return `{
3 | "podcasts": [],
4 | "superAdmins": [
5 | "vZY2XY1RD9HIfWi8ift-1_DnHLDadZMWrufSh-_rKF0",
6 | "kaYP9bJtpqON8Kyy3RbqnqdtDBDUsPTQTNUCvZtKiFI"
7 | ],
8 | "maintainers": [],
9 | "oracle_address": "8K77MdQ855XCjdbwAO-SjeB89z3tlWGQYDowAHR45pA",
10 | "contractOwner": "${address}",
11 | "ownerSwapped": false,
12 | "limitations": {
13 | "podcast_name_len": {
14 | "min": 2,
15 | "max": 500
16 | },
17 | "podcast_desc_len": {
18 | "min": 10,
19 | "max": 15000
20 | },
21 | "author_name_len": {
22 | "min": 2,
23 | "max": 150
24 | },
25 | "lang_char_code": {
26 | "min": 2,
27 | "max": 2
28 | },
29 | "categories": {
30 | "min": 1,
31 | "max": 300
32 | },
33 | "episode_name_len": {
34 | "min": 3,
35 | "max": 500
36 | },
37 | "episode_desc_len": {
38 | "min": 1,
39 | "max": 5000
40 | }
41 | }
42 | }`;
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils/podcast.js:
--------------------------------------------------------------------------------
1 | export const fetchPodcasts = async () => {
2 | const json = await (
3 | await fetch("https://whispering-retreat-94540.herokuapp.com/feeds/podcasts")
4 | ).json();
5 | return json.res;
6 | };
7 |
8 | export const sortPodcasts = async (filters) => {
9 | let url = "https://whispering-retreat-94540.herokuapp.com/feeds/podcasts/sort/";
10 | let result = [];
11 |
12 | // Basically, this sandwiches all possible filter requests into one
13 | await Promise.all(filters.map(async (filter) => {
14 | result[filter] = await fetch(url+filter).then(res => res.json()).then(json => json.res);
15 | }))
16 |
17 | return result;
18 | };
19 |
--------------------------------------------------------------------------------
/src/utils/shorthands.js:
--------------------------------------------------------------------------------
1 | import Swal from 'sweetalert2'
2 | import { arweave, FEE_MULTIPLIER } from '../utils/arweave.js'
3 | import { getStorageTable } from 'arweave-fees.js'
4 |
5 | export const swal = (t, status="success", txt="", extraText="") => {
6 | Swal.fire({
7 | title: t(`${txt}.title`),
8 | text: t(`${txt}.text`) + `${extraText}`,
9 | icon: status,
10 | customClass: "font-mono",
11 | })
12 | }
13 |
14 | export async function fetchWalletAddress() {
15 | await window.arweaveWallet.connect(["ACCESS_ADDRESS", "SIGN_TRANSACTION", "SIGNATURE"])
16 | let addr = await window.arweaveWallet.getActiveAddress()
17 |
18 | if (!addr) {
19 | await window.arweaveWallet.connect(["ACCESS_ADDRESS"]);
20 | addr = await window.arweaveWallet.getActiveAddress();
21 | }
22 |
23 | return addr
24 | }
25 |
26 | const readFileAsync = (file) => {
27 | return new Promise((resolve, reject) => {
28 | let reader = new FileReader();
29 |
30 | reader.onload = () => {
31 | resolve(reader.result);
32 | };
33 |
34 | reader.onerror = reject;
35 | reader.readAsArrayBuffer(file);
36 | })
37 | }
38 |
39 | export async function processFile(file) {
40 | try {
41 | let contentBuffer = await readFileAsync(file);
42 | return contentBuffer
43 | } catch (err) {
44 | console.log(err);
45 | }
46 | }
47 |
48 | export async function calculateStorageFee(bytes) {
49 | const storagePrices = await getStorageTable()
50 | let costPerMB = storagePrices['MB'] ? storagePrices['MB'].ar : 0
51 | if (costPerMB === 0 || bytes === 0) return 0
52 | let cost = bytes * (costPerMB / 1024 / 1024)
53 | if (storagePrices['KB'].ar >= cost) {
54 | console.log('Size too small')
55 | cost = storagePrices['KB'].ar
56 | }
57 | return cost
58 | }
59 |
60 | export async function userHasEnoughAR (t, bytes, serviceFee) {
61 | let address = await fetchWalletAddress()
62 | let failText = 'generalerrors.'
63 | if (!address) return swal(t, 'error', failText + 'cantfindaddress')
64 |
65 | // this query returns balance in Winston units
66 | let balance = await arweave.wallets.getBalance(address).then((balance) => balance)
67 |
68 | let balanceInAR = balance * 1e-12
69 | let cost = await calculateStorageFee(bytes)
70 | if (cost === 0) swal(t, 'error', failText + 'cantfetchprices')
71 | cost = (cost * FEE_MULTIPLIER) + serviceFee
72 |
73 | let repr = cost.toFixed(2)
74 | console.log('this operation will cost (with x3 mining fee + serviceFee): ' + cost)
75 | console.log('this wallet has ' + balanceInAR)
76 | if (balanceInAR >= cost) return "all good"
77 | else return swal(t, 'error', failText + 'lowbalance', `${repr}` + ' AR')
78 | }
79 |
--------------------------------------------------------------------------------
/src/utils/theme.js:
--------------------------------------------------------------------------------
1 | export function isDarkMode() {
2 | return localStorage.getItem("theme") === "dark";
3 | }
4 |
--------------------------------------------------------------------------------
/src/yellow-rec.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | important: true,
3 | content: ["./src/**/*.{js,jsx,ts,tsx}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [
8 | require("@tailwindcss/aspect-ratio"),
9 | require("@tailwindcss/typography"),
10 | require("daisyui"),
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/v2-contracts/v2.js:
--------------------------------------------------------------------------------
1 | /**
2 | * SWC used as first level data registery for
3 | * Arweave hosted podcasts.
4 | *
5 | * website: permacast.net
6 | *
7 | * Contract Version: V2
8 | *
9 | * contributor(s): charmful0x
10 | *
11 | * Licence: MIT
12 | **/
13 |
14 | export async function handle(state, action) {
15 | const input = action.input;
16 | const caller = action.caller;
17 | const podcasts = state.podcasts;
18 |
19 | // ERRORS List
20 | const ERROR_INVALID_CALLER =
21 | "the caller is not allowed to execute this function";
22 | const ERROR_INVALID_PRIMITIVE_TYPE =
23 | "the given data is not a corrected primitive type per function";
24 | const ERROR_INVALID_STRING_LENGTH =
25 | "the string is out of the allowed length ranges";
26 | const ERROR_NOT_A_DATA_TX = "the transaction is not an Arweave TX DATA";
27 | const ERROR_MIME_TYPE = "the given mime type is not supported";
28 | const ERROR_UNSUPPORTED_LANG = "the given language code is not supported";
29 | const ERROR_REQUIRED_PARAMETER = "the function still require a parameter";
30 | const ERROR_INVALID_NUMBER_TYPE = "only inetegers are allowed";
31 | const ERROR_NEGATIVE_INTEGER =
32 | "negative integer was supplied when only positive Intare allowed";
33 | const ERROR_EPISODE_INDEX_NOT_FOUND =
34 | "there is no episode with the given index";
35 | const ERROR_PODCAST_INDEX_NOT_FOUND =
36 | "there is no podcast with the given index";
37 | const ERROR_OLD_VALUE_EQUAL_TO_NEW = "old valueand new value are equal";
38 |
39 | if (input.function === "createPodcast") {
40 | const name = input.name;
41 | const author = input.author;
42 | const desc = input.desc;
43 | const lang = input.lang;
44 | const isExplicit = input.isExplicit;
45 | const categories = input.categories;
46 | const email = input.email;
47 | const cover = input.cover;
48 |
49 | const pid = SmartWeave.transaction.id;
50 |
51 | await _getContractOwner(true, caller);
52 |
53 | _validateStringTypeLen(name, 3, 50);
54 | _validateStringTypeLen(author, 2, 50);
55 | _validateStringTypeLen(desc, 10, 750);
56 | _validateStringTypeLen(email, 0, 320);
57 | _validateStringTypeLen(categories, 3, 150);
58 | _validateStringTypeLen(cover, 43, 43);
59 | _validateStringTypeLen(lang, 2, 2);
60 |
61 | await _validateDataTransaction(cover, "image/");
62 |
63 | if (!["yes", "no"].includes(isExplicit)) {
64 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE);
65 | }
66 |
67 | podcasts.push({
68 | pid: pid,
69 | index: _getPodcastIndex(), // id equals the index of the podacast obj in the podcasts array
70 | childOf: SmartWeave.contract.id,
71 | owner: caller,
72 | podcastName: name,
73 | author: author,
74 | email: email,
75 | description: desc,
76 | language: lang,
77 | explicit: isExplicit,
78 | categories: categories.split(",").map((category) => category.trim()),
79 | cover: cover,
80 | episodes: [],
81 | logs: [pid],
82 | });
83 |
84 | return { state };
85 | }
86 |
87 | if (input.function === "addEpisode") {
88 | const index = input.index; // podcasts index
89 | const name = input.name;
90 | const audio = input.audio; // the TXID of 'audio/' data
91 | const desc = input.desc;
92 |
93 | await _getContractOwner(true, caller);
94 |
95 | _validateStringTypeLen(name, 3, 50);
96 | _validateStringTypeLen(audio, 43, 43);
97 | _validateStringTypeLen(desc, 0, 250);
98 | _validateInteger(index, true);
99 |
100 | const TxMetadata = await _validateDataTransaction(audio, "audio/");
101 |
102 | if (!podcasts[index]) {
103 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND);
104 | }
105 |
106 | podcasts[index]["episodes"].push({
107 | eid: SmartWeave.transaction.id,
108 | childOf: index,
109 | episodeName: name,
110 | description: desc,
111 | audioTx: audio,
112 | audioTxByteSize: Number.parseInt(TxMetadata.size),
113 | type: TxMetadata.type,
114 | uploadedAt: SmartWeave.block.timestamp,
115 | logs: [SmartWeave.transaction.id],
116 | });
117 |
118 | return { state };
119 | }
120 |
121 | // PODCAST ACTIONS:
122 |
123 | if (input.function === "editPodcastName") {
124 | const index = input.index;
125 | const name = input.name;
126 |
127 | const actionTx = SmartWeave.transaction.id;
128 |
129 | await _getContractOwner(true, caller);
130 | _validateStringTypeLen(name, 3, 50);
131 | _validateInteger(index, true);
132 |
133 | if (!podcasts[index]) {
134 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND);
135 | }
136 |
137 | if (podcasts[index]["podcastName"] === name) {
138 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW);
139 | }
140 |
141 | podcasts[index]["podcastName"] = name;
142 | podcasts[index]["logs"].push(actionTx);
143 |
144 | return { state };
145 | }
146 |
147 | if (input.function === "editPodcastDesc") {
148 | const index = input.index;
149 | const desc = input.desc;
150 |
151 | const actionTx = SmartWeave.transaction.id;
152 |
153 | await _getContractOwner(true, caller);
154 | _validateInteger(index, true);
155 | _validateStringTypeLen(desc, 10, 750);
156 |
157 | if (!podcasts[index]) {
158 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND);
159 | }
160 |
161 | if (podcasts[index]["description"] === desc) {
162 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW);
163 | }
164 |
165 | podcasts[index]["description"] = desc;
166 | podcasts[index]["logs"].push(actionTx);
167 |
168 | return { state };
169 | }
170 |
171 | if (input.function === "editPodcastCover") {
172 | const index = input.index;
173 | const cover = input.cover;
174 | const actionTx = SmartWeave.transaction.id;
175 | const tagsMap = new Map();
176 |
177 | await _getContractOwner(true, caller);
178 | _validateStringTypeLen(cover, 43, 43);
179 | _validateInteger(index, true);
180 |
181 | if (!podcasts[index]) {
182 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND);
183 | }
184 |
185 | if (podcasts[index]["cover"] === cover) {
186 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW);
187 | }
188 |
189 | await _validateDataTransaction(cover, "image/");
190 |
191 | podcasts[index]["cover"] = cover;
192 | podcasts[index]["logs"].push(actionTx);
193 |
194 | return { state };
195 | }
196 |
197 | // EPISODES ACTIONS:
198 |
199 | if (input.function === "editEpisodeName") {
200 | const name = input.name;
201 | const index = input.index; //podcast index
202 | const id = input.id; // episode's index
203 |
204 | const actionTx = SmartWeave.transaction.id;
205 |
206 | await _getContractOwner(true, caller);
207 |
208 | _validateStringTypeLen(name, 2, 50);
209 | _validateInteger(index, true);
210 | _validateInteger(id, true);
211 | _validateEpisodeExistence(index, id);
212 |
213 | if (podcasts[index]["episodes"][id]["episodeName"] === name) {
214 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW);
215 | }
216 |
217 | podcasts[index]["episodes"][id]["episodeName"] = name;
218 | podcasts[index]["episodes"][id]["logs"].push(actionTx);
219 |
220 | return { state };
221 | }
222 |
223 | if (input.function === "editEpisodeDesc") {
224 | const index = input.index;
225 | const id = input.id;
226 | const desc = input.desc;
227 |
228 | const actionTx = SmartWeave.transaction.id;
229 |
230 | await _getContractOwner(true, caller);
231 |
232 | _validateStringTypeLen(desc, 25, 500);
233 | _validateInteger(index, true);
234 | _validateInteger(id, true);
235 | _validateEpisodeExistence(index, id);
236 |
237 | if (podcasts[index]["episodes"][id]["description"] === desc) {
238 | throw new ContractError(ERROR_OLD_VALUE_EQUAL_TO_NEW);
239 | }
240 |
241 | podcasts[index]["episodes"][id]["description"] = desc;
242 | podcasts[index]["episodes"][id]["logs"].push(actionTx);
243 |
244 | return { state };
245 | }
246 |
247 | // HELPER FUNCTIONS:
248 | function _getPodcastIndex() {
249 | if (podcasts.length === 0) {
250 | return 0;
251 | }
252 |
253 | return podcasts.length;
254 | }
255 |
256 | function _validateStringTypeLen(str, minLen, maxLen) {
257 | if (typeof str !== "string") {
258 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE);
259 | }
260 |
261 | if (str.length < minLen || str.length > maxLen) {
262 | throw new ContractError(ERROR_INVALID_STRING_LENGTH);
263 | }
264 | }
265 |
266 | function _validateInteger(number, allowNull) {
267 | if (typeof allowNull === "undefined") {
268 | throw new ContractError(ERROR_REQUIRED_PARAMETER);
269 | }
270 |
271 | if (!Number.isInteger(number)) {
272 | throw new ContractError(ERROR_INVALID_NUMBER_TYPE);
273 | }
274 |
275 | if (allowNull) {
276 | if (number < 0) {
277 | throw new ContractError(ERROR_NEGATIVE_INTEGER);
278 | }
279 | } else if (number <= 0) {
280 | throw new ContractError(ERROR_INVALID_NUMBER_TYPE);
281 | }
282 | }
283 |
284 | async function _getContractOwner(validate, caller) {
285 | const contractID = SmartWeave.contract.id;
286 | const contractTxObject = await SmartWeave.unsafeClient.transactions.get(
287 | contractID
288 | );
289 | const base64Owner = contractTxObject["owner"];
290 | const contractOwner = await SmartWeave.unsafeClient.wallets.ownerToAddress(
291 | base64Owner
292 | );
293 |
294 | if (validate && contractOwner !== caller) {
295 | throw new ContractError(ERROR_INVALID_CALLER);
296 | }
297 |
298 | return contractOwner;
299 | }
300 |
301 | async function _validateDataTransaction(tx, mimeType) {
302 | const tagsMap = new Map();
303 | const transaction = await SmartWeave.unsafeClient.transactions.get(tx);
304 | const tags = transaction.get("tags");
305 |
306 | for (let tag of tags) {
307 | const key = tag.get("name", { decode: true, string: true });
308 | const value = tag.get("value", { decode: true, string: true });
309 | tagsMap.set(key, value);
310 | }
311 |
312 | if (!tagsMap.has("Content-Type")) {
313 | throw new ContractError(ERROR_NOT_A_DATA_TX);
314 | }
315 |
316 | if (!tagsMap.get("Content-Type").startsWith(mimeType)) {
317 | throw new ContractError(ERROR_MIME_TYPE);
318 | }
319 |
320 | return {
321 | size: transaction.data_size,
322 | type: tagsMap.get("Content-Type"),
323 | };
324 | }
325 |
326 | function _validateEpisodeExistence(index, id) {
327 | if (!podcasts[index]) {
328 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND);
329 | }
330 |
331 | if (!podcasts[index]["episodes"][id]) {
332 | throw new ContractError(ERROR_EPISODE_INDEX_NOT_FOUND);
333 | }
334 | }
335 |
336 | throw new ContractError("unknow function supplied: ", input.function);
337 | }
--------------------------------------------------------------------------------
/v2-contracts/v2.json:
--------------------------------------------------------------------------------
1 | {
2 | "podcasts": []
3 | }
--------------------------------------------------------------------------------
/v3/oracle/oracle.js:
--------------------------------------------------------------------------------
1 | export async function handle(state, action) {
2 |
3 | const caller = action.caller;
4 | const input = action.input;
5 |
6 | // STATE VARIABLES
7 | const admins = state.admins;
8 | const limitations = state.limitations;
9 |
10 | // ERRORS LIST
11 | const ERROR_INVALID_CALLER =
12 | "non-permissioned address has invoked the function";
13 | const NON_INTEGER_NUMBER_PASSED = "only integer numbers are allowed";
14 | const ERROR_MISSING_ARGUMENTS =
15 | "at least one limitations argument is required to invoke this function";
16 | const ERROR_MIN_GREATER_THAN_MAX =
17 | "min limitation cannot be greater than max limitation";
18 |
19 | if (input.function === "updateLimitations") {
20 | /**
21 | * @dev update the oracle's limitations (contract state).
22 | * Only the contract admins can invoke this function.
23 | *
24 | * @param podcast_name_len podcast's name lengh limits as [min, max] array
25 | * @param podcast_desc_len podcast's description lengh limits as [min, max] array
26 | * @param author_name_len author's name lengh limits as [min, max] array
27 | * @param lang_char_code depends on the ISO used. also [min, max] array.
28 | * @param categories categories words length. should be compatible with RSS
29 | * feeds generating web2 audio platforms (Spotify, iTunes). also [min, max]
30 | * @param episode_name_len episode's name lengh limits as [min, max] array
31 | * @param nepisode_desc_len episode's description lengh limits as [min, max] array
32 | *
33 | **/
34 |
35 | _isAdmin(caller);
36 |
37 | if (Object.keys(input).length < 2) {
38 | throw new ContractError(ERROR_MISSING_ARGUMENTS);
39 | }
40 |
41 | for (let inputName of Object.keys(input)) {
42 | if (inputName in limitations) {
43 | // min/max are expected to be passed in an array of values
44 | const min = _validateInteger(input[inputName][0]);
45 | const max = _validateInteger(input[inputName][1]);
46 |
47 | // prevent human errors
48 | if (min > max) {
49 | throw new ContractError(ERROR_MIN_GREATER_THAN_MAX);
50 | }
51 |
52 | limitations[inputName].min = min;
53 | limitations[inputName].max = max;
54 | }
55 | }
56 |
57 | return { state };
58 | }
59 |
60 | // HELPER FNCTIONS
61 | function _isAdmin(address) {
62 | if (!admins.includes(address)) {
63 | throw new ContractError(ERROR_INVALID_CALLER);
64 | }
65 | }
66 |
67 | function _validateInteger(nb) {
68 | if (typeof nb !== "number" && !Number.isInteger(nb)) {
69 | throw new ContractError(NON_INTEGER_NUMBER_PASSED);
70 | }
71 |
72 | return nb;
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/v3/oracle/oracle.json:
--------------------------------------------------------------------------------
1 | {
2 | "admins": [
3 | "vZY2XY1RD9HIfWi8ift-1_DnHLDadZMWrufSh-_rKF0",
4 | "kaYP9bJtpqON8Kyy3RbqnqdtDBDUsPTQTNUCvZtKiFI"
5 | ],
6 | "limitations": {
7 | "podcast_name_len": {
8 | "min": 2,
9 | "max": 500
10 | },
11 | "podcast_desc_len":{
12 | "min": 10,
13 | "max": 15000
14 | },
15 | "author_name_len": {
16 | "min": 2,
17 | "max": 150
18 | },
19 | "lang_char_code": {
20 | "min": 2,
21 | "max": 2
22 | },
23 | "categories": {
24 | "min": 1,
25 | "max": 300
26 | },
27 | "episode_name_len": {
28 | "min": 3,
29 | "max": 500
30 | },
31 | "episode_desc_len": {
32 | "min": 1,
33 | "max": 5000
34 | }
35 | }
36 | }
--------------------------------------------------------------------------------
/v3/v3.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | *
4 | *
5 | *
6 | *
7 | * ██████╗░███████╗██████╗░███╗░░░███╗░█████╗░░█████╗░░█████╗░░██████╗████████╗
8 | * ██╔══██╗██╔════╝██╔══██╗████╗░████║██╔══██╗██╔══██╗██╔══██╗██╔════╝╚══██╔══╝
9 | * ██████╔╝█████╗░░██████╔╝██╔████╔██║███████║██║░░╚═╝███████║╚█████╗░░░░██║░░░
10 | * ██╔═══╝░██╔══╝░░██╔══██╗██║╚██╔╝██║██╔══██║██║░░██╗██╔══██║░╚═══██╗░░░██║░░░
11 | * ██║░░░░░███████╗██║░░██║██║░╚═╝░██║██║░░██║╚█████╔╝██║░░██║██████╔╝░░░██║░░░
12 | * ╚═╝░░░░░╚══════╝╚═╝░░╚═╝╚═╝░░░░░╚═╝╚═╝░░╚═╝░╚════╝░╚═╝░░╚═╝╚═════╝░░░░╚═╝░░░
13 | *
14 | * SWC as first level data registery for Arweave hosted podcasts.
15 | *
16 | * @version TESTNET V3 - Amber Version
17 | * @author charmful0x
18 | * @website permacast.net
19 | * @license MIT
20 | **/
21 |
22 | export async function handle(state, action) {
23 | const input = action.input;
24 | const caller = action.caller;
25 |
26 | // STATE
27 | const podcasts = state.podcasts;
28 | const maintainers = state.maintainers;
29 | const superAdmins = state.superAdmins;
30 | const limitations = state.limitations;
31 | const oracle_address = state.oracle_address;
32 |
33 | // LIMITATION METADATA ACCESS
34 | const POD_NAME_LIMITS = limitations["podcast_name_len"];
35 | const POD_DESC_LIMITS = limitations["podcast_desc_len"];
36 | const EP_NAME_LIMITS = limitations["episode_name_len"];
37 | const EP_DESC_LIMITS = limitations["episode_desc_len"];
38 | const AUTHOR_NAME_LIMITS = limitations["author_name_len"];
39 | const LANG_CHAR_LIMITS = limitations["lang_char_code"];
40 | const CATEGORY_LIMITS = limitations["categories"];
41 |
42 | // ERRORS List
43 | const ERROR_INVALID_CALLER =
44 | "the caller is not allowed to execute this function";
45 | const ERROR_INVALID_PRIMITIVE_TYPE =
46 | "the given data is not a corrected primitive type per function";
47 | const ERROR_INVALID_STRING_LENGTH =
48 | "the string is out of the allowed length ranges";
49 | const ERROR_NOT_A_DATA_TX = "the transaction is not an Arweave TX DATA";
50 | const ERROR_MIME_TYPE = "the given mime type is not supported";
51 | const ERROR_UNSUPPORTED_LANG = "the given language code is not supported";
52 | const ERROR_REQUIRED_PARAMETER = "the function still require a parameter";
53 | const ERROR_INVALID_NUMBER_TYPE = "only inetegers are allowed";
54 | const ERROR_NEGATIVE_INTEGER =
55 | "negative integer was supplied when only positive Intare allowed";
56 | const ERROR_EPISODE_INDEX_NOT_FOUND =
57 | "there is no episode with the given index";
58 | const ERROR_PODCAST_INDEX_NOT_FOUND =
59 | "there is no podcast with the given index";
60 | const ERROR_OLD_VALUE_EQUAL_TO_NEW = "old valueand new value are equal";
61 | const ERROR_MAINTAINER_ALREADY_ADDED =
62 | "the address has been already added as maintainer";
63 | const ERROR_MAINTAINER_NOT_FOUND =
64 | "cannot find a mainatiner with the given address";
65 | const ERROR_SUPER_ADMIN_ALREADY_ADDED =
66 | "the given address has been already added as super admin";
67 | const ERROR_SUPER_ADMIN_NOT_EXIST = "the given address is not a super admin";
68 | const ERROR_V2_SRC_NOT_VALID = "the given factoryID does not have the V2 SRC";
69 | const ERROR_MIGRATION_DONE = "the contract has already migrated a V2 state";
70 | const ERROR_CANNOT_MIGRATE_TO_ACTIVE_FACTORY =
71 | "this contract has been already activated - cannot override the state";
72 | const ERROR_STATE_CANNOT_MIGRATE = "error while retrieving the V2 state";
73 | const ERROR_PID_NOT_FOUND = "cannot find a podcast with the given PID";
74 | const ERROR_EID_NOT_FOUND = "cannot find an episode with the given EID";
75 | const ERROR_PID_OF_EID_NOT_FOUND = "cannot find a podcast with the given EID";
76 | const ERROR_PODCAST_UPLOAD_DUPLICATED =
77 | "auto-minimal protection from double podcast creation activated";
78 | const ERROR_EPISODE_UPLOAD_DUPLICATED =
79 | "auto-minimal protection from double episode uploads activated";
80 | const ERROR_OWNER_ALREADY_SWAPPED =
81 | "contract owner can be swapped one time only";
82 | const ERROR_INVALID_ARWEAVE_ADDRESS = "invalid Arweave address";
83 |
84 | if (input.function === "createPodcast") {
85 | /**
86 | * @dev create a podcast object and append it to
87 | * the smart contract state. Only the factory's
88 | * superAdmins have the permission to invoke it.
89 | *
90 | * @param name podcast name
91 | * @param author podcast author
92 | * @param lang language char code (ISO 639-1:2002)
93 | * @param isExplicit indicates the existence of
94 | * explicit content, used for RSS feed generating
95 | * @param categories RSS supported categories strings
96 | * @param email author email address
97 | * @param cover Arweave data TXID of type `image/*`
98 | * @param contentType 'a' or 'v' indication audio or
99 | * video (content type) that will be supported in this podcast.
100 | *
101 | * @return state
102 | **/
103 |
104 | const name = input.name;
105 | const author = input.author;
106 | const lang = input.lang;
107 | const isExplicit = input.isExplicit;
108 | const categories = input.categories;
109 | const email = input.email;
110 | const cover = input.cover;
111 |
112 | const contentType = input.contentType === "a" ? "audio/" : "video/"
113 |
114 | await _isSuperAdmin(true, caller);
115 |
116 | const pid = SmartWeave.transaction.id;
117 | const desc = await _handleDescription(
118 | pid,
119 | POD_DESC_LIMITS.min,
120 | POD_DESC_LIMITS.max
121 | );
122 |
123 | _validateStringTypeLen(name, POD_NAME_LIMITS.min, POD_NAME_LIMITS.max);
124 | _validateStringTypeLen(
125 | author,
126 | AUTHOR_NAME_LIMITS.min,
127 | AUTHOR_NAME_LIMITS.max
128 | );
129 | _validateStringTypeLen(email, 0, 320);
130 | _validateStringTypeLen(
131 | categories,
132 | CATEGORY_LIMITS.min,
133 | CATEGORY_LIMITS.max
134 | );
135 | _validateStringTypeLen(cover, 43, 43);
136 | _validateStringTypeLen(lang, LANG_CHAR_LIMITS.min, LANG_CHAR_LIMITS.max);
137 | // auto-prevent double podcast creation
138 | _checkPodcastUploadDuplication(name);
139 | await _validateDataTransaction(cover, "image/");
140 |
141 | if (!["yes", "no"].includes(isExplicit)) {
142 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE);
143 | }
144 |
145 | podcasts.push({
146 | pid: pid,
147 | contentType: contentType,
148 | createdAtBlockheight: SmartWeave.block.height, // blockheight - V3 metadata
149 | createdAt: SmartWeave.block.timestamp, // block's timestamp
150 | index: _getPodcastIndex(), // id equals the index of the podacast obj in the podcasts array
151 | childOf: SmartWeave.contract.id,
152 | owner: caller,
153 | podcastName: name,
154 | author: author,
155 | email: email,
156 | description: desc,
157 | language: lang,
158 | explicit: isExplicit,
159 | categories: categories.split(",").map((category) => category.trim()),
160 | cover: cover,
161 | isVisible: true,
162 | episodes: [],
163 | logs: [pid],
164 | });
165 |
166 | return { state };
167 | }
168 |
169 | if (input.function === "addEpisode") {
170 | /**
171 | * @dev create an episode object and append
172 | * it to a podcast object's episodes array
173 | * Maintainers and SuperAdmins can invoke
174 | * this function.
175 | *
176 | * @param pid podcast ID (pid). 43 chars string
177 | * @param name episode name
178 | * @param content episode audio's or video's Arweave TXID
179 | *
180 | * @return state
181 | **/
182 |
183 | const pid = input.pid;
184 | const name = input.name;
185 | const content = input.content;
186 |
187 | await _getMaintainers(true, caller);
188 |
189 | const eid = SmartWeave.transaction.id;
190 |
191 | // episode's description is extracted from the
192 | // interaction's TX body data.
193 | const desc = await _handleDescription(
194 | eid,
195 | EP_DESC_LIMITS.min,
196 | EP_DESC_LIMITS.max
197 | );
198 |
199 | _validateStringTypeLen(name, EP_NAME_LIMITS.min, EP_NAME_LIMITS.max);
200 | _validateStringTypeLen(content, 43, 43);
201 | _validateStringTypeLen(pid, 43, 43);
202 |
203 | const pidIndex = _getAndValidatePidIndex(pid);
204 | // auto prevent double episode uploads
205 | _checkEpisodeUploadDuplication(pidIndex, name);
206 |
207 | const contentType = podcasts[pidIndex]["contentType"];
208 |
209 | const TxMetadata = await _validateDataTransaction(content, contentType);
210 |
211 | podcasts[pidIndex]["episodes"].push({
212 | eid: SmartWeave.transaction.id,
213 | childOf: pidIndex,
214 | episodeName: name,
215 | description: desc,
216 | contentTx: content,
217 | contentTxByteSize: Number.parseInt(TxMetadata.size),
218 | type: TxMetadata.type,
219 | uploader: caller,
220 | uploadedAt: SmartWeave.block.timestamp,
221 | uploadedAtBlockheight: SmartWeave.block.height, // V3 metadata
222 | isVisible: true,
223 | logs: [SmartWeave.transaction.id],
224 | });
225 |
226 | return { state };
227 | }
228 |
229 | // PODCAST ACTIONS:
230 | // PERMISSIONED TO THE CONTRACT
231 | // OWNER (DEPLOYER) ONLY
232 |
233 | if (input.function === "updatePodcastMetadata") {
234 | /**
235 | * @dev update a podcast's object metadata that is
236 | * already created. Only superAdmins can invoke it.
237 | *
238 | * @param name podcast name
239 | * @param author podcast author
240 | * @param lang language char code (ISO 639-1:2002)
241 | * @param isExplicit indicates the existence of
242 | * explicit content, used for RSS feed generating
243 | * @param categories RSS supported categories strings
244 | * @param email author email address
245 | * @param cover Arweave data TXID of type `image/*`
246 | *
247 | * @return state
248 | **/
249 |
250 | const pid = input.pid;
251 | const name = input.name;
252 | const cover = input.cover;
253 | const author = input.author;
254 | const email = input.email;
255 | const lang = input.lang;
256 | const categories = input.categories;
257 | const isExplicit = input.isExplicit;
258 |
259 | // boolean - if true, get and validate
260 | // the interaction body TX data ( text/* )
261 | let desc = input.desc;
262 |
263 | await _isSuperAdmin(true, caller);
264 |
265 | const pidIndex = _getAndValidatePidIndex(pid);
266 | const actionTx = SmartWeave.transaction.id;
267 |
268 | if (name) {
269 | _validateStringTypeLen(name, POD_NAME_LIMITS.min, POD_NAME_LIMITS.max);
270 | podcasts[pidIndex]["podcastName"] = name;
271 | }
272 |
273 | if (desc) {
274 | desc = await _handleDescription(
275 | actionTx,
276 | POD_DESC_LIMITS.min,
277 | POD_DESC_LIMITS.max
278 | );
279 | podcasts[pidIndex]["description"] = desc;
280 | }
281 |
282 | if (author) {
283 | _validateStringTypeLen(
284 | author,
285 | AUTHOR_NAME_LIMITS.min,
286 | AUTHOR_NAME_LIMITS.max
287 | );
288 | podcasts[pidIndex]["author"] = author;
289 | }
290 |
291 | if (email) {
292 | _validateStringTypeLen(email, 0, 320);
293 | podcasts[pidIndex]["email"] = email;
294 | }
295 |
296 | if (lang) {
297 | _validateStringTypeLen(lang, LANG_CHAR_LIMITS.min, LANG_CHAR_LIMITS.max);
298 | podcasts[pidIndex]["language"] = lang;
299 | }
300 |
301 | if (categories) {
302 | _validateStringTypeLen(
303 | categories,
304 | CATEGORY_LIMITS.min,
305 | CATEGORY_LIMITS.max
306 | );
307 | podcasts[pidIndex]["categories"] = categories
308 | .split(",")
309 | .map((category) => category.trim());
310 | }
311 |
312 | if (isExplicit) {
313 | if (!["yes", "no"].includes(isExplicit)) {
314 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE);
315 | }
316 |
317 | podcasts[pidIndex]["explicit"] = isExplicit;
318 | }
319 |
320 | if (cover) {
321 | await _validateDataTransaction(cover, "image/");
322 | podcasts[pidIndex]["cover"] = cover;
323 | }
324 |
325 | podcasts[pidIndex]["logs"].push(actionTx);
326 |
327 | return { state };
328 | }
329 |
330 | // PERMISSION: CONTRACT OWNER (DEPLOYER)
331 |
332 | if (input.function === "addMaintainer") {
333 | /**
334 | * @dev add an address the the maintainers array.
335 | * Only the contract owner (deployer) has permission.
336 | *
337 | * @param address the new maintainer address
338 | *
339 | * @return state
340 | **/
341 |
342 | const address = input.address;
343 |
344 | await _getContractOwner(true, caller);
345 | _validateAddress(address);
346 |
347 | if (maintainers.includes(address)) {
348 | throw new ContractError(ERROR_MAINTAINER_ALREADY_ADDED);
349 | }
350 |
351 | maintainers.push(address);
352 | return { state };
353 | }
354 |
355 | if (input.function === "removeMaintainer") {
356 | /**
357 | * @dev remove a maintainer from the the maintainers array.
358 | * Only the contract owner (deployer) has permission.
359 | *
360 | * @param address the address of to-remove maintainer
361 | *
362 | * @return state
363 | **/
364 |
365 | const address = input.address;
366 |
367 | _validateAddress(address);
368 |
369 | // a maintainer can remove himself or get removed by the sc owner
370 | if (address !== caller && caller !== SmartWeave.contract.owner) {
371 | throw new ContractError(ERROR_INVALID_CALLER);
372 | }
373 |
374 | if (!maintainers.includes(address)) {
375 | throw new ContractError(ERROR_MAINTAINER_NOT_FOUND);
376 | }
377 |
378 | maintainers.splice(
379 | maintainers.findIndex((m) => m === address),
380 | 1
381 | );
382 |
383 | return { state };
384 | }
385 |
386 | if (input.function === "addSuperAdmin") {
387 | /**
388 | * @dev add an address the the superAdmins array.
389 | * Only the contract owner (deployer) has permission.
390 | *
391 | * @param address the new superAdmin address
392 | *
393 | * @return state
394 | **/
395 |
396 | const address = input.address;
397 |
398 | await _getContractOwner(true, caller);
399 | _validateAddress(address);
400 |
401 | if (superAdmins.includes(address)) {
402 | throw new ContractError(ERROR_SUPER_ADMIN_ALREADY_ADDED);
403 | }
404 |
405 | superAdmins.push(address);
406 |
407 | return { state };
408 | }
409 |
410 | if (input.function === "removeSuperAdmin") {
411 | /**
412 | * @dev remove a superAdmin from the the superAdmins array.
413 | * Only the contract owner (deployer) has permission.
414 | *
415 | * @param address the address of to-remove maintainer
416 | *
417 | * @return state
418 | **/
419 |
420 | const address = input.address;
421 |
422 | _validateAddress(address);
423 |
424 | // a superAdmin can remove himself or get removed by the sc owner
425 | if (caller !== address && caller !== SmartWeave.contract.owner) {
426 | throw new ContractError(ERROR_INVALID_CALLER);
427 | }
428 |
429 | if (!superAdmins.includes(address)) {
430 | throw new ContractError(ERROR_SUPER_ADMIN_NOT_EXIST);
431 | }
432 |
433 | superAdmins.splice(
434 | superAdmins.findIndex((m) => m === address),
435 | 1
436 | );
437 |
438 | return { state };
439 | }
440 |
441 | if (input.function === "reverseVisibility") {
442 | /**
443 | * @dev reverse the visibility of a podcast/episode.
444 | * it just reverse a boolean value of the `isVisibile`
445 | * property. The podcast/episode cannot be removed
446 | * from the factory state.
447 | *
448 | * Only superAdmins can invoke this function.
449 | *
450 | * @param type is the object type, "eid" or "pid"
451 | * @param id PID of type "pid" or EID of type "eid"
452 | *
453 | * @return state
454 | *
455 | **/
456 |
457 | const type = input.type;
458 | const id = input.id;
459 |
460 | await _isSuperAdmin(true, caller);
461 |
462 | if (type === "pid") {
463 | const pidIndex = _getAndValidatePidIndex(id);
464 | const currentVisibility = podcasts[pidIndex].isVisible;
465 | podcasts[pidIndex].isVisible = !currentVisibility;
466 |
467 | return { state };
468 | }
469 |
470 | if (type === "eid") {
471 | const pid = _getPidOfEid(id);
472 | const pidIndex = _getAndValidatePidIndex(pid);
473 | const eidIndex = _getAndValidateEidIndex(id, pidIndex);
474 |
475 | const currentVisibility =
476 | podcasts[pidIndex]["episodes"][eidIndex].isVisible;
477 | podcasts[pidIndex]["episodes"][eidIndex].isVisible = !currentVisibility;
478 |
479 | return { state };
480 | }
481 |
482 | throw new ContractError(ERROR_INVALID_CALLER);
483 | }
484 |
485 | // EPISODES ACTIONS:
486 | // PERMISSIONED TO THE CONTRACT
487 | // OWNER AND MAINTAINERS
488 |
489 | if (input.function === "updateEpisodeMetadata") {
490 | /**
491 | * @dev update the episode's metadata.
492 | * Maintainers and SuperAdmins can invoke
493 | * this function.
494 | *
495 | * @param eid episode ID (eid). 43 chars string
496 | * @param name episode name
497 | * @param audio episode audio's Arweave TXID
498 | * @param desc is a boolean that's when set to true,
499 | * the episode's description is extracted from the
500 | * interaction TXID body data.
501 | *
502 | * @return state
503 | *
504 | **/
505 |
506 | const eid = input.eid;
507 | const name = input.name;
508 | let desc = input.desc;
509 |
510 | await _getMaintainers(true, caller);
511 |
512 | const pid = _getPidOfEid(eid);
513 | const pidIndex = _getAndValidatePidIndex(pid);
514 | const eidIndex = _getAndValidateEidIndex(eid, pidIndex);
515 | const actionTx = SmartWeave.transaction.id;
516 |
517 | if (name) {
518 | _validateStringTypeLen(name, EP_NAME_LIMITS.min, EP_NAME_LIMITS.max);
519 | podcasts[pidIndex]["episodes"][eidIndex]["episodeName"] = name;
520 | }
521 |
522 | if (desc) {
523 | desc = await _handleDescription(
524 | actionTx,
525 | EP_DESC_LIMITS.min,
526 | EP_DESC_LIMITS.max
527 | );
528 | podcasts[pidIndex]["episodes"][eidIndex]["description"] = desc;
529 | }
530 |
531 | podcasts[pidIndex]["episodes"][eidIndex]["logs"].push(actionTx);
532 |
533 | return { state };
534 | }
535 |
536 | if (input.function === "swapOwner") {
537 | /**
538 | * @dev this function is invoked one time only after V2 <> V3 migration
539 | * the purpose of this function is to deploy & migrate V2 on behalf
540 | * of the V2 factory owner, then give him/her back full ownership.
541 | *
542 | * @param address the address of the new contract owner.
543 | *
544 | * @return state
545 | **/
546 |
547 | const address = input.address;
548 |
549 | _validateAddress(address);
550 | await _isSuperAdmin(true, caller);
551 |
552 | // contract owner can be swapped for once only
553 | if (!state.ownerSwapped) {
554 | SmartWeave.contract.owner = address;
555 | state.ownerSwapped = true;
556 | state.contractOwner = address;
557 | return { state };
558 | }
559 |
560 | throw new ContractError(ERROR_OWNER_ALREADY_SWAPPED);
561 | }
562 |
563 | if (input.function === "fetchOracle") {
564 | /**
565 | * @dev read the oracle's state and update the limitations
566 | * object of the factory (contract's state).
567 | *
568 | * @return state
569 | *
570 | **/
571 |
572 | await _getContractOwner(true, caller);
573 |
574 | const oracleState = await SmartWeave.contracts.readContractState(
575 | state.oracle_address
576 | );
577 |
578 | // update the limitations according to the oracle;
579 | state.limitations = oracleState.limitations;
580 |
581 | return { state };
582 | }
583 |
584 | if (input.function === "updateOracleAddress") {
585 | /**
586 | * @dev contract owner can update the factory's
587 | * oracle address to his/her own oracle. The initial
588 | * factory's state come with an oracle provided from
589 | * Permacast protocol developers. The new oracle state
590 | * must be backward compatible with the original oracle,
591 | *
592 | * @param address the new oracle address
593 | *
594 | * @return state
595 | *
596 | **/
597 |
598 | const address = input.address;
599 |
600 | await _getContractOwner(true, caller);
601 | _validateAddress(address);
602 |
603 | state.oracle_address = address;
604 |
605 | return { state };
606 | }
607 |
608 | // HELPER FUNCTIONS:
609 | function _getPodcastIndex() {
610 | if (podcasts.length === 0) {
611 | return 0;
612 | }
613 |
614 | return podcasts.length;
615 | }
616 |
617 | function _validateAddress(address) {
618 | _validateStringTypeLen(address, 43, 43);
619 |
620 | if (!/[a-z0-9_-]{43}/i.test(address)) {
621 | throw new ContractError(ERROR_INVALID_ARWEAVE_ADDRESS);
622 | }
623 | }
624 |
625 | function _validateStringTypeLen(str, minLen, maxLen) {
626 | if (typeof str !== "string") {
627 | throw new ContractError(ERROR_INVALID_PRIMITIVE_TYPE);
628 | }
629 |
630 | if (str.trim().length < minLen || str.trim().length > maxLen) {
631 | throw new ContractError(ERROR_INVALID_STRING_LENGTH);
632 | }
633 | }
634 |
635 | function _validateInteger(number, allowNull) {
636 | if (typeof allowNull === "undefined") {
637 | throw new ContractError(ERROR_REQUIRED_PARAMETER);
638 | }
639 |
640 | if (!Number.isInteger(number)) {
641 | throw new ContractError(ERROR_INVALID_NUMBER_TYPE);
642 | }
643 |
644 | if (allowNull) {
645 | if (number < 0) {
646 | throw new ContractError(ERROR_NEGATIVE_INTEGER);
647 | }
648 | } else if (number <= 0) {
649 | throw new ContractError(ERROR_INVALID_NUMBER_TYPE);
650 | }
651 | }
652 |
653 | async function _getContractOwner(validate, caller) {
654 | const contractOwner = SmartWeave.contract.owner;
655 | if (validate && contractOwner !== caller) {
656 | throw new ContractError(ERROR_INVALID_CALLER);
657 | }
658 | }
659 |
660 | async function _getMaintainers(validate, address) {
661 | const superAdmins = await _isSuperAdmin(false);
662 | const contractMaintainers = superAdmins.concat(maintainers);
663 | // remove any address duplication amongst different privileges
664 | const filteredMaintainers = Array.from(new Set(contractMaintainers));
665 |
666 | if (validate && !filteredMaintainers.includes(address)) {
667 | throw new ContractError(ERROR_INVALID_CALLER);
668 | }
669 |
670 | return filteredMaintainers;
671 | }
672 |
673 | async function _validateDataTransaction(tx, mimeType) {
674 | const tagsMap = new Map();
675 | const transaction = await SmartWeave.unsafeClient.transactions.get(tx);
676 | const tags = transaction.get("tags");
677 |
678 | for (let tag of tags) {
679 | const key = tag.get("name", { decode: true, string: true });
680 | const value = tag.get("value", { decode: true, string: true });
681 | tagsMap.set(key, value);
682 | }
683 |
684 | if (!tagsMap.has("Content-Type")) {
685 | throw new ContractError(ERROR_NOT_A_DATA_TX);
686 | }
687 |
688 | if (!tagsMap.get("Content-Type").startsWith(mimeType)) {
689 | throw new ContractError(ERROR_MIME_TYPE);
690 | }
691 |
692 | return {
693 | size: transaction.data_size,
694 | type: tagsMap.get("Content-Type"),
695 | };
696 | }
697 |
698 | function _validateEpisodeExistence(index, id) {
699 | if (!podcasts[index]) {
700 | throw new ContractError(ERROR_PODCAST_INDEX_NOT_FOUND);
701 | }
702 |
703 | if (!podcasts[index]["episodes"][id]) {
704 | throw new ContractError(ERROR_EPISODE_INDEX_NOT_FOUND);
705 | }
706 | }
707 |
708 | function _getAndValidatePidIndex(pid) {
709 | const index = podcasts.findIndex((podcast) => podcast["pid"] === pid);
710 |
711 | if (index >= 0) {
712 | return index;
713 | }
714 |
715 | throw new ContractError(ERROR_PID_NOT_FOUND);
716 | }
717 |
718 | function _getAndValidateEidIndex(eid, pidIndex) {
719 | const index = podcasts[pidIndex].episodes.findIndex(
720 | (episode) => episode["eid"] === eid
721 | );
722 |
723 | if (index >= 0) {
724 | return index;
725 | }
726 |
727 | throw new ContractError(ERROR_EID_NOT_FOUND);
728 | }
729 |
730 | function _getPidOfEid(eid) {
731 | const pid = podcasts.find((podcast) =>
732 | podcast.episodes.find((episode) => episode["eid"] === eid)
733 | )?.["pid"];
734 |
735 | if (!pid) {
736 | throw new ContractError(ERROR_PID_OF_EID_NOT_FOUND);
737 | }
738 |
739 | return pid;
740 | }
741 |
742 | async function _isSuperAdmin(validate, address) {
743 | const contractOwner = SmartWeave.contract.owner;
744 | const allAdmins = superAdmins.concat(contractOwner);
745 |
746 | // remove any address duplication amongst different privileges
747 | const filteredSupers = Array.from(new Set(allAdmins));
748 |
749 | if (validate && !filteredSupers.includes(address)) {
750 | throw new ContractError(ERROR_INVALID_CALLER);
751 | }
752 |
753 | return filteredSupers;
754 | }
755 |
756 | function _checkPodcastUploadDuplication(name) {
757 | const duplicationIndex = podcasts.findIndex(
758 | (podcast) => podcast["podcastName"] === name
759 | );
760 | if (duplicationIndex === -1) {
761 | return false;
762 | }
763 |
764 | const duplicationBlockheight =
765 | podcasts[duplicationIndex]?.createdAtBlockheight;
766 | // backward compatibility with V2 migrations
767 | if (!duplicationBlockheight) {
768 | return false;
769 | }
770 |
771 | if (duplicationBlockheight + 5 > SmartWeave.block.height) {
772 | throw new ContractError(ERROR_PODCAST_UPLOAD_DUPLICATED);
773 | }
774 |
775 | return false;
776 | }
777 |
778 | function _checkEpisodeUploadDuplication(pidIndex, name) {
779 | const duplicationIndex = podcasts[pidIndex].episodes.findIndex(
780 | (episode) => episode["episodeName"] === name
781 | );
782 | if (duplicationIndex === -1) {
783 | return false;
784 | }
785 |
786 | const duplicationBlockheight =
787 | podcasts[pidIndex]["episodes"][duplicationIndex]?.uploadedAtBlockheight;
788 | // backward compatibility with V2 migrations
789 | if (!duplicationBlockheight) {
790 | return false;
791 | }
792 |
793 | if (duplicationBlockheight + 5 > SmartWeave.block.height) {
794 | throw new ContractError(ERROR_EPISODE_UPLOAD_DUPLICATED);
795 | }
796 |
797 | return false;
798 | }
799 |
800 | async function _handleDescription(txid, min, max) {
801 | if (/[a-z0-9_-]{43}/i.test(txid)) {
802 | const tagsMap = new Map();
803 | const txObject = await SmartWeave.unsafeClient.transactions.get(txid);
804 |
805 | const tags = txObject.get("tags");
806 |
807 | for (let tag of tags) {
808 | const key = tag.get("name", { decode: true, string: true });
809 | const value = tag.get("value", { decode: true, string: true });
810 | tagsMap.set(key, value);
811 | }
812 |
813 | if (!tagsMap.has("Content-Type")) {
814 | throw new ContractError(ERROR_NOT_A_DATA_TX);
815 | }
816 |
817 | if (!tagsMap.get("Content-Type").startsWith("text/")) {
818 | throw new ContractError(ERROR_MIME_TYPE);
819 | }
820 |
821 | const data = await SmartWeave.unsafeClient.transactions.getData(txid, {
822 | decode: true,
823 | string: true,
824 | });
825 |
826 | _validateStringTypeLen(data, min, max);
827 | return data;
828 | }
829 |
830 | throw new ContractError(ERROR_INVALID_STRING_LENGTH);
831 | }
832 |
833 | throw new ContractError("unknow function supplied: ", input.function);
834 | }
835 |
--------------------------------------------------------------------------------
/v3/v3.json:
--------------------------------------------------------------------------------
1 | {
2 | "podcasts": [],
3 | "superAdmins": [],
4 | "maintainers": [],
5 | "oracle_address": "8K77MdQ855XCjdbwAO-SjeB89z3tlWGQYDowAHR45pA",
6 | "contractOwner": "ARWEAVE_ADDRESS",
7 | "ownerSwapped": false,
8 | "limitations": {
9 | "podcast_name_len": {
10 | "min": 2,
11 | "max": 500
12 | },
13 | "podcast_desc_len": {
14 | "min": 10,
15 | "max": 15000
16 | },
17 | "author_name_len": {
18 | "min": 2,
19 | "max": 150
20 | },
21 | "lang_char_code": {
22 | "min": 2,
23 | "max": 2
24 | },
25 | "categories": {
26 | "min": 1,
27 | "max": 300
28 | },
29 | "episode_name_len": {
30 | "min": 3,
31 | "max": 500
32 | },
33 | "episode_desc_len": {
34 | "min": 1,
35 | "max": 5000
36 | }
37 | }
38 | }
39 |
40 |
41 |
--------------------------------------------------------------------------------