├── .dockerignore ├── .gitignore ├── Dockerfile ├── src ├── package.json ├── index.js └── package-lock.json ├── README.md └── docker-compose.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | src/node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/node_modules 2 | src/.env 3 | .vscode 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | ADD src /app 4 | WORKDIR /app 5 | RUN npm install 6 | 7 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lemmy-subscriber", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "start-dev": "node_modules/.bin/nodemon index.js", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "lemmy-js-client": "0.19.6" 15 | }, 16 | "devDependencies": { 17 | "nodemon": "^3.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lemmy Community Seeder (LCS) 2 | 3 | When launching a new Lemmy instance, your **All** feed will have very little populated. Also as a small instance, new communities that crop up may never make their way to you. LCS is a tool to seed communities, so your users have something in their **All** feed, right from the start. It tells your instance to pull the top communities and the communities with the top posts from your favorite instances (and subscribe to them with the specified user). 4 | 5 | ## Documentation 6 | 7 | Latest documentation available at: [https://nowsci.com/lemmy/lcs/](https://nowsci.com/lemmy/lcs/) 8 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | 5 | lcs: 6 | image: nowsci/lcs 7 | container_name: lcs 8 | environment: 9 | LOCAL_URL: https://lemmy.domain.ext 10 | LOCAL_USERNAME: user 11 | LOCAL_PASSWORD: password 12 | REMOTE_INSTANCES: '[ 13 | "lemmy.world", 14 | "lemmy.ml", 15 | "sh.itjust.works", 16 | "lemmy.one" ]' 17 | POST_COUNT: 50 18 | COMMUNITY_COUNT: 20 19 | COMMUNITY_SORT_METHODS: '[ 20 | "TopAll", 21 | "TopDay" ]' 22 | COMMUNITY_TYPE: All 23 | IGNORE: '[ 24 | "feddit.de" ]' 25 | SECONDS_AFTER_COMMUNITY_ADD: 17 26 | MINUTES_BETWEEN_RUNS: 1440 27 | restart: unless-stopped 28 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { LemmyHttp } = require("lemmy-js-client"); 2 | const fs = require('fs').promises; 3 | 4 | const localUrl = process.env.LOCAL_URL; 5 | const localUsername = process.env.LOCAL_USERNAME; 6 | const localPassword = process.env.LOCAL_PASSWORD; 7 | 8 | const trackerFile = process.env.TRACKER_FILE ? process.env.TRACKER_FILE : null; 9 | let trackerCommunities = []; 10 | 11 | // List from https://github.com/maltfield/awesome-lemmy-instances 12 | const remoteInstances = JSON.parse(process.env.REMOTE_INSTANCES); 13 | 14 | const postCount = parseInt(process.env.POST_COUNT); 15 | const communityCount = parseInt(process.env.COMMUNITY_COUNT); 16 | // https://github.com/LemmyNet/lemmy-js-client/blob/main/src/types/SortType.ts 17 | // https://join-lemmy.org/docs/users/03-votes-and-ranking.html 18 | const communitySortMethods = JSON.parse(process.env.COMMUNITY_SORT_METHODS); 19 | const communityType = process.env.COMMUNITY_TYPE; 20 | const ignore = process.env.IGNORE ? JSON.parse(process.env.IGNORE) : []; 21 | const unsubscribe = process.env.UNSUBSCRIBE ? JSON.parse(process.env.UNSUBSCRIBE) : []; 22 | const secondsAfterCommunityAdd = parseInt( 23 | process.env.SECONDS_AFTER_COMMUNITY_ADD 24 | ); 25 | const minutesBetweenRuns = parseInt(process.env.MINUTES_BETWEEN_RUNS); 26 | const nsfw = process.env.NSFW ? JSON.parse(process.env.NSFW) : false; 27 | 28 | function sleep(s) { 29 | return new Promise((resolve) => setTimeout(resolve, s * 1000)); 30 | } 31 | 32 | async function check(localClient, user, items) { 33 | const searched = []; 34 | for await (const item of items) { 35 | try { 36 | if (!item.community.deleted) { 37 | if (nsfw || !item.community.nsfw) { 38 | let existingCommunity; 39 | const communityInstance = item.community.actor_id.split("/")[2]; 40 | const community = `${item.community.name}@${communityInstance}`; 41 | 42 | if (!ignore.some((v) => community.includes(v))) { 43 | if (!searched.includes(community)) { 44 | try { 45 | existingCommunity = await localClient.getCommunity({ 46 | name: `${community}`, 47 | }); 48 | console.log(`Searched for community ${community}: found`); 49 | } catch (e) { 50 | console.log(`Searched for community ${community}: ${e}`); 51 | } 52 | 53 | try { 54 | 55 | // If the community is not found on the local instance add it and follow it 56 | if (!existingCommunity) { 57 | let newCommunity = await localClient.resolveObject({ 58 | q: item.community.actor_id, 59 | auth: user.jwt, 60 | }); 61 | console.log( 62 | `Added community ${community}: ${newCommunity.community.community.id}` 63 | ); 64 | 65 | console.log(`Sleeping ${secondsAfterCommunityAdd} seconds`); 66 | await sleep(secondsAfterCommunityAdd); 67 | 68 | await localClient.followCommunity({ 69 | community_id: newCommunity.community.community.id, 70 | follow: true, 71 | }); 72 | console.log( 73 | `Followed community ${community}: ${newCommunity.community.community.id}` 74 | ); 75 | } 76 | 77 | if (trackerFile && !trackerCommunities.includes(community)) { 78 | console.log(`Adding community ${community} to trackerment file`); 79 | await fs.appendFile(trackerFile, `[${new Date().toISOString()}] ${community}\n`); 80 | trackerCommunities.push(community); 81 | } 82 | 83 | searched.push(`${community}`); 84 | } catch (e) { 85 | console.log(`Couldn't add community ${community}: ${e}`); 86 | } 87 | } 88 | } else { 89 | console.log(`Ignoring community ${community}`); 90 | } 91 | } 92 | } 93 | } catch (e) { 94 | console.log(e); 95 | } 96 | } 97 | } 98 | 99 | async function subscribeAll(localClient, user) { 100 | let page = 1; 101 | let loops = 0; 102 | 103 | while (loops < 500) { 104 | try { 105 | let response = await localClient.listCommunities({ 106 | type_: communityType, 107 | sort: "Old", 108 | page: page, 109 | limit: 50, 110 | auth: user.jwt, 111 | }); 112 | 113 | if (!response.communities || response.communities.length == 0) { 114 | break; 115 | } 116 | 117 | for await (const c of response.communities) { 118 | if ( 119 | !unsubscribe && 120 | c.subscribed == "NotSubscribed" && 121 | !c.blocked && 122 | !c.community.removed && 123 | !c.community.deleted 124 | ) { 125 | if (nsfw || !c.community.nsfw) { 126 | console.log(`Subscribing to ${c.community.actor_id}`); 127 | await localClient.followCommunity({ 128 | community_id: c.community.id, 129 | follow: true, 130 | }); 131 | } else { 132 | console.log(`Already subscribed to ${c.community.actor_id}`); 133 | } 134 | } 135 | 136 | if ( 137 | c.subscribed != "NotSubscribed" && 138 | ( 139 | unsubscribe 140 | || (c.blocked || c.community.removed || c.community.deleted) 141 | || (!nsfw && c.community.nsfw) 142 | ) 143 | ) { 144 | console.log(`Unsubscribing from ${c.community.actor_id}`); 145 | await localClient.followCommunity({ 146 | community_id: c.community.id, 147 | follow: false, 148 | auth: user.jwt, 149 | }); 150 | await sleep(3); 151 | } 152 | } 153 | } catch (e) { 154 | console.log(e); 155 | } 156 | page++; 157 | loops++; 158 | } 159 | } 160 | 161 | async function main() { 162 | if (trackerFile) { 163 | try { 164 | const fileContent = await fs.readFile(trackerFile, 'utf8'); 165 | const trackerCommunitiesWithDate = fileContent.split('\n'); 166 | trackerCommunities = trackerCommunitiesWithDate 167 | .map(item => item.split(' ')[1]) 168 | .filter(item => item !== undefined); 169 | } catch (err) { 170 | if (err.code !== 'ENOENT') throw err; 171 | } 172 | console.log(`${trackerCommunities.length} communities already tracked`); 173 | } 174 | while (true) { 175 | try { 176 | let localClient = new LemmyHttp(localUrl); 177 | let loginForm = { 178 | username_or_email: localUsername, 179 | password: localPassword, 180 | }; 181 | let user = await localClient.login(loginForm); 182 | localClient.setHeaders({ Authorization: "Bearer " + user.jwt }); 183 | if (trackerFile || (!trackerFile && !unsubscribe)) { 184 | for await (const remoteInstance of remoteInstances) { 185 | try { 186 | for await (const communitySortMethod of communitySortMethods) { 187 | let remoteClient = new LemmyHttp(`https://${remoteInstance}`); 188 | 189 | console.log( 190 | `Checking ${remoteInstance} for posts of ${communitySortMethod}` 191 | ); 192 | let response = await remoteClient.getPosts({ 193 | type_: communityType, 194 | sort: communitySortMethod, 195 | limit: postCount, 196 | }); 197 | await check(localClient, user, response.posts); 198 | 199 | console.log( 200 | `Checking ${remoteInstance} for communities of ${communitySortMethod}` 201 | ); 202 | response = await remoteClient.listCommunities({ 203 | type_: communityType, 204 | sort: communitySortMethod, 205 | limit: communityCount, 206 | }); 207 | await check(localClient, user, response.communities); 208 | } 209 | } catch (e) { 210 | console.log(e); 211 | } 212 | } 213 | } 214 | await subscribeAll(localClient, user); 215 | } catch (e) { 216 | console.log(e); 217 | } 218 | console.log(`Sleeping ${minutesBetweenRuns} minutes`); 219 | await sleep(minutesBetweenRuns * 60); 220 | } 221 | } 222 | 223 | main(); 224 | -------------------------------------------------------------------------------- /src/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lemmy-subscriber", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "lemmy-subscriber", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "lemmy-js-client": "^0.19.0-alpha.18" 13 | } 14 | }, 15 | "node_modules/asynckit": { 16 | "version": "0.4.0", 17 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 18 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 19 | }, 20 | "node_modules/combined-stream": { 21 | "version": "1.0.8", 22 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 23 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 24 | "dependencies": { 25 | "delayed-stream": "~1.0.0" 26 | }, 27 | "engines": { 28 | "node": ">= 0.8" 29 | } 30 | }, 31 | "node_modules/cross-fetch": { 32 | "version": "3.1.6", 33 | "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", 34 | "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", 35 | "dependencies": { 36 | "node-fetch": "^2.6.11" 37 | } 38 | }, 39 | "node_modules/delayed-stream": { 40 | "version": "1.0.0", 41 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 42 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 43 | "engines": { 44 | "node": ">=0.4.0" 45 | } 46 | }, 47 | "node_modules/dotenv": { 48 | "version": "16.3.1", 49 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", 50 | "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", 51 | "engines": { 52 | "node": ">=12" 53 | }, 54 | "funding": { 55 | "url": "https://github.com/motdotla/dotenv?sponsor=1" 56 | } 57 | }, 58 | "node_modules/form-data": { 59 | "version": "4.0.0", 60 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 61 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 62 | "dependencies": { 63 | "asynckit": "^0.4.0", 64 | "combined-stream": "^1.0.8", 65 | "mime-types": "^2.1.12" 66 | }, 67 | "engines": { 68 | "node": ">= 6" 69 | } 70 | }, 71 | "node_modules/lemmy-js-client": { 72 | "version": "0.19.0-alpha.18", 73 | "resolved": "https://registry.npmjs.org/lemmy-js-client/-/lemmy-js-client-0.19.0-alpha.18.tgz", 74 | "integrity": "sha512-cKJfKKnjK+ijk0Yd6ydtne3Y4FILp2RbQg05pCru9n6PCyPAa85eQL4QxPB1PPed20ckSZRcHLcnr/bYFDgpaw==", 75 | "dependencies": { 76 | "cross-fetch": "^3.1.5", 77 | "form-data": "^4.0.0" 78 | } 79 | }, 80 | "node_modules/mime-db": { 81 | "version": "1.52.0", 82 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 83 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 84 | "engines": { 85 | "node": ">= 0.6" 86 | } 87 | }, 88 | "node_modules/mime-types": { 89 | "version": "2.1.35", 90 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 91 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 92 | "dependencies": { 93 | "mime-db": "1.52.0" 94 | }, 95 | "engines": { 96 | "node": ">= 0.6" 97 | } 98 | }, 99 | "node_modules/node-fetch": { 100 | "version": "2.6.11", 101 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", 102 | "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", 103 | "dependencies": { 104 | "whatwg-url": "^5.0.0" 105 | }, 106 | "engines": { 107 | "node": "4.x || >=6.0.0" 108 | }, 109 | "peerDependencies": { 110 | "encoding": "^0.1.0" 111 | }, 112 | "peerDependenciesMeta": { 113 | "encoding": { 114 | "optional": true 115 | } 116 | } 117 | }, 118 | "node_modules/tr46": { 119 | "version": "0.0.3", 120 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 121 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 122 | }, 123 | "node_modules/webidl-conversions": { 124 | "version": "3.0.1", 125 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 126 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 127 | }, 128 | "node_modules/whatwg-url": { 129 | "version": "5.0.0", 130 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 131 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 132 | "dependencies": { 133 | "tr46": "~0.0.3", 134 | "webidl-conversions": "^3.0.0" 135 | } 136 | } 137 | }, 138 | "dependencies": { 139 | "asynckit": { 140 | "version": "0.4.0", 141 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 142 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 143 | }, 144 | "combined-stream": { 145 | "version": "1.0.8", 146 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 147 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 148 | "requires": { 149 | "delayed-stream": "~1.0.0" 150 | } 151 | }, 152 | "cross-fetch": { 153 | "version": "3.1.6", 154 | "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.6.tgz", 155 | "integrity": "sha512-riRvo06crlE8HiqOwIpQhxwdOk4fOeR7FVM/wXoxchFEqMNUjvbs3bfo4OTgMEMHzppd4DxFBDbyySj8Cv781g==", 156 | "requires": { 157 | "node-fetch": "^2.6.11" 158 | } 159 | }, 160 | "delayed-stream": { 161 | "version": "1.0.0", 162 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 163 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" 164 | }, 165 | "dotenv": { 166 | "version": "16.3.1", 167 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", 168 | "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" 169 | }, 170 | "form-data": { 171 | "version": "4.0.0", 172 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", 173 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", 174 | "requires": { 175 | "asynckit": "^0.4.0", 176 | "combined-stream": "^1.0.8", 177 | "mime-types": "^2.1.12" 178 | } 179 | }, 180 | "lemmy-js-client": { 181 | "version": "0.19.0-alpha.18", 182 | "resolved": "https://registry.npmjs.org/lemmy-js-client/-/lemmy-js-client-0.19.0-alpha.18.tgz", 183 | "integrity": "sha512-cKJfKKnjK+ijk0Yd6ydtne3Y4FILp2RbQg05pCru9n6PCyPAa85eQL4QxPB1PPed20ckSZRcHLcnr/bYFDgpaw==", 184 | "requires": { 185 | "cross-fetch": "^3.1.5", 186 | "form-data": "^4.0.0" 187 | } 188 | }, 189 | "mime-db": { 190 | "version": "1.52.0", 191 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 192 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 193 | }, 194 | "mime-types": { 195 | "version": "2.1.35", 196 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 197 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 198 | "requires": { 199 | "mime-db": "1.52.0" 200 | } 201 | }, 202 | "node-fetch": { 203 | "version": "2.6.11", 204 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", 205 | "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", 206 | "requires": { 207 | "whatwg-url": "^5.0.0" 208 | } 209 | }, 210 | "tr46": { 211 | "version": "0.0.3", 212 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", 213 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" 214 | }, 215 | "webidl-conversions": { 216 | "version": "3.0.1", 217 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", 218 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" 219 | }, 220 | "whatwg-url": { 221 | "version": "5.0.0", 222 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", 223 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", 224 | "requires": { 225 | "tr46": "~0.0.3", 226 | "webidl-conversions": "^3.0.0" 227 | } 228 | } 229 | } 230 | } 231 | --------------------------------------------------------------------------------