├── readme_assets ├── token.png ├── copy-id.png └── preview.png ├── test.bat ├── package.json ├── MMM-DiscordWatch.css ├── .gitignore ├── README.md ├── node_helper.js ├── MMM-DiscordWatch.js └── LICENSE /readme_assets/token.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smiche/MMM-DiscordWatch/HEAD/readme_assets/token.png -------------------------------------------------------------------------------- /readme_assets/copy-id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smiche/MMM-DiscordWatch/HEAD/readme_assets/copy-id.png -------------------------------------------------------------------------------- /readme_assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Smiche/MMM-DiscordWatch/HEAD/readme_assets/preview.png -------------------------------------------------------------------------------- /test.bat: -------------------------------------------------------------------------------- 1 | docker run -d ^ 2 | --publish 8089:8080 ^ 3 | --restart always ^ 4 | --volume D:/magic_mirror/config:/opt/magic_mirror/config ^ 5 | --volume D:/magic_mirror/modules:/opt/magic_mirror/modules ^ 6 | --name magic_mirror ^ 7 | bastilimbach/docker-magicmirror -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mmm-discordwatch", 3 | "version": "0.2.0", 4 | "description": "MagicMirror2 module for watching discord channels.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "MMM", 11 | "magicmirror", 12 | "discord" 13 | ], 14 | "author": "Smiche", 15 | "license": "CC Zero", 16 | "dependencies": { 17 | "discord.js": "^13.6.0" 18 | }, 19 | "engines": { 20 | "node": "^16.x" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MMM-DiscordWatch.css: -------------------------------------------------------------------------------- 1 | .MMM-DiscordWatch .symbol { 2 | padding-left: 10px; 3 | padding-right: 0px; 4 | font-size: 80%; 5 | vertical-align: top; 6 | } 7 | 8 | .MMM-DiscordWatch .symbol span { 9 | display: inline-block; 10 | transform: translate(0, 2px); 11 | } 12 | 13 | .MMM-DiscordWatch .author { 14 | vertical-align: top; 15 | border-right-width: 1px; 16 | border-right-style: groove; 17 | padding-right: 5px; 18 | } 19 | 20 | .MMM-DiscordWatch .message { 21 | padding-left: 5px; 22 | padding-right: 0; 23 | text-align: left; 24 | } 25 | 26 | .MMM-DiscordWatch .time { 27 | padding-left: 30px; 28 | text-align: right; 29 | vertical-align: top; 30 | } 31 | 32 | .MMM-DiscordWatch .small .author { 33 | vertical-align: top; 34 | width: 40px; 35 | height: 40px; 36 | margin-right: 10px; 37 | } 38 | 39 | .MMM-DiscordWatch .medium .author { 40 | vertical-align: top; 41 | width: 40px; 42 | height: 40px; 43 | margin-right: 10px; 44 | } 45 | 46 | .MMM-DiscordWatch .large .author { 47 | vertical-align: middle; 48 | width: 80px; 49 | height: 80px; 50 | margin-right: 10px; 51 | } 52 | 53 | .MMM-DiscordWatch .small .time { 54 | min-width: 130px; 55 | } 56 | 57 | .MMM-DiscordWatch .medium .time { 58 | min-width: 190px; 59 | } 60 | 61 | .MMM-DiscordWatch .large .time { 62 | min-width: 400px; 63 | } 64 | 65 | .MMM-DiscordWatch .small .channel { 66 | font-size: 10px; 67 | line-height: 20px; 68 | 69 | padding-left: 5px; 70 | padding-right: 0; 71 | text-align: left; 72 | } 73 | 74 | .MMM-DiscordWatch .medium .channel { 75 | font-size: 15px; 76 | line-height: 15px; 77 | 78 | padding-left: 5px; 79 | padding-right: 0; 80 | text-align: left; 81 | } 82 | 83 | .MMM-DiscordWatch .large .channel { 84 | font-size: 30px; 85 | line-height: 32px; 86 | 87 | padding-left: 5px; 88 | padding-right: 0; 89 | text-align: left; 90 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MMM-DiscordWatch 2 | This is a module for displaying messages from Discord channels on [MagicMirror](https://github.com/MichMich/MagicMirror). 3 | 4 | ![Preview](readme_assets/preview.png) 5 | 6 | The module uses discord's bot API and the npm package [discord.js](https://discord.js.org/). 7 | 8 | ### Installation 9 | 10 | 1. Go to discord developer and create a new application and a bot. Obtain the bot token and also invite the bot to your server. [(Discord Developer docs)](https://discord.com/developers/docs/intro) 11 | ![Discord token](readme_assets/token.png) 12 | 2. Clone the repository in your `modules` folder: 13 | ``` 14 | git clone https://github.com/Smiche/MMM-DiscordWatch.git 15 | ``` 16 | 3. Go inside the `MMM-DiscordWatch` directory and run: 17 | ``` 18 | npm install 19 | ``` 20 | 4. Right click on the discord channels you're interest in and copy their ID, then add it to the `subscribedChannels` property: 21 | 22 | 23 | ![copy-id](readme_assets/copy-id.png) 24 | 25 | 26 | Example config: 27 | 28 | 29 | ```js 30 | { 31 | module: "MMM-DiscordWatch", 32 | position: "top_right", 33 | header: "Discord", 34 | config: { 35 | discordToken: "", 36 | tableClass: "small", 37 | maxEntries: 10, 38 | maxMessageLength: 25, 39 | maxMessageLines: 1, 40 | wrapEvents: false, 41 | fade: true, 42 | fadePoint: 0.25, 43 | showChannel: true, 44 | subscribedChannels: ["123456891373953034","123456038079119360"], //2 channels to listen to for example 45 | } 46 | }, 47 | ``` 48 | 49 | ### Configuration 50 | 51 | | Name | Description | Default value | 52 | |----------------|---------------------------------|-------------| 53 | | `discordToken` | A discord bot token. | **false** | 54 | | `tableClass` | Size of the table, only small is tested and verified. `small/medium/large` | **small** | 55 | | `maxEntries` | Amount of messages to list. | **10**| 56 | | `maxMessageLength` | How many symbols of text can be shown. | **25** | 57 | | `maxAuthorLength` | Maximum length for author nick shown. | **8** | 58 | | `maxMessageLines` | Maximum lines a message can use, if `wrapEvents` is true. | **1** | 59 | | `wrapEvents` | Defines if messages should be wrapped to a new line or not. | **false** | 60 | | `fade` | Used to fade the last messages. | **true** | 61 | | `fadePoint` | Location of the fade. | **0.25** | 62 | | `showChannel` | Whether to show the source channel of the message or not. | **true** | 63 | | `subscribedChannels` | Array of channels that the module will listen to | **[]** | 64 | -------------------------------------------------------------------------------- /node_helper.js: -------------------------------------------------------------------------------- 1 | const NodeHelper = require("node_helper"); 2 | const Log = require("../../js/logger"); 3 | const { Client, Intents } = require('discord.js'); 4 | 5 | module.exports = NodeHelper.create({ 6 | // Override start method. 7 | start: function () { 8 | Log.log("Starting node helper for: " + this.name); 9 | this.fetchers = []; 10 | }, 11 | 12 | // Override socketNotificationReceived method. 13 | socketNotificationReceived: function (notification, payload) { 14 | console.log("notification received."); 15 | if (notification === "ADD_DISCORD_CONFIG") { 16 | this.createDiscordClient(payload.config, payload.id); 17 | } 18 | }, 19 | 20 | createDiscordClient: function (config, identifier) { 21 | let self = this; 22 | console.log("connecting to discord."); 23 | // no token provided, we exit 24 | if (!config.discordToken) { 25 | self.sendSocketNotification("ERROR", { id: identifier, err: "Missing discord token." }); 26 | return; 27 | } 28 | 29 | const client = new Client({ 30 | intents: Object.keys(Intents.FLAGS) 31 | }); 32 | 33 | client.on('ready', () => { 34 | console.debug(`Logged in as ${client.user.tag}!`); 35 | self.sendSocketNotification("CONNECTED", { id: identifier }); 36 | //request the last x messages 37 | this.requestLastMessages(client, config, identifier); 38 | }); 39 | 40 | client.on('messageCreate', msg => { 41 | if (config.subscribedChannels.indexOf(msg.channel.id) > -1) { 42 | self.sendSocketNotification("NEW_MESSAGE", { id: identifier, text: msg.content, author: msg.author.username, channel: msg.channel.name, createdAt: msg.createdAt }) 43 | } 44 | }); 45 | 46 | client.login(config.discordToken).catch(err => { 47 | console.error(err); 48 | self.sendSocketNotification("ERROR", { id: identifier, err: err }); 49 | }); 50 | }, 51 | 52 | /** 53 | * This will attempt to fetch maxEntries amount of messages from each subscribed channel, 54 | * then sort by date and submit the newest maxEntries amount of messages. 55 | * 56 | * @param {*} client discord client that is already connected preferably 57 | * @param {*} config 58 | */ 59 | requestLastMessages: function (client, config, identifier) { 60 | let self = this; 61 | let channels = []; 62 | config.subscribedChannels.forEach((ChId) => { 63 | channels.push(client.channels.cache.get(ChId)); 64 | }) 65 | 66 | let promises = []; 67 | for (const Channel of channels) promises.push(Channel.messages.fetch({ limit: config.maxEntries })); 68 | 69 | Promise.all(promises).then((messagesArray) => { 70 | let messages = []; 71 | messagesArray.forEach(arr => { 72 | messages = messages.concat(...arr.values()); 73 | }); 74 | //console.log("MessArr:", messages); 75 | messages = messages.sort((a, b) => { 76 | return new Date(b.createdAt) - new Date(a.createdAt); 77 | }) 78 | //console.log("Sorted?"); 79 | messages.splice(config.maxEntries); 80 | //console.log(messages); 81 | messages = messages.map((msg) => { 82 | return { text: msg.content, author: msg.author.username, channel: msg.channel.name, createdAt: msg.createdAt }; 83 | }); 84 | self.sendSocketNotification("NEW_MESSAGE", { id: identifier, messages: messages }); 85 | }).catch(err => { 86 | console.error(err); 87 | }); 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /MMM-DiscordWatch.js: -------------------------------------------------------------------------------- 1 | Module.register("MMM-DiscordWatch", { 2 | // Default module config. 3 | defaults: { 4 | discordToken: false, 5 | tableClass: "small", 6 | maxEntries: 10, 7 | maxMessageLength: 25, 8 | maxAuthorLength: 8, 9 | maxMessageLines: 1, 10 | wrapEvents: false, // wrap events to multiple lines breaking at maxMessageLength 11 | fade: true, 12 | fadePoint: 0.25, 13 | showChannel: true, 14 | subscribedChannels: [], 15 | }, 16 | 17 | // Define required scripts. 18 | getStyles: function () { 19 | return ["MMM-DiscordWatch.css", "font-awesome.css"]; 20 | }, 21 | 22 | getScripts: function () { 23 | return ["moment.js"]; 24 | }, 25 | start: function () { 26 | Log.log("Starting module: " + this.name); 27 | this.addDiscord(this.config); 28 | 29 | this.messages = []; 30 | this.loaded = false; 31 | }, 32 | // Override socket notification handler. 33 | socketNotificationReceived: function (notification, payload) { 34 | if (this.identifier !== payload.id) return; 35 | 36 | if (notification === "NEW_MESSAGE") { 37 | if (payload.messages) this.messages = this.messages.concat(payload.messages); 38 | else { 39 | this.messages.pop(); 40 | this.messages.unshift(payload); 41 | } 42 | this.loaded = true; 43 | } else if (notification === "ERROR") { 44 | Log.error("Error:", payload.err); 45 | this.loaded = true; 46 | } else if (notification === "CONNECTED") this.loaded = true; 47 | this.updateDom(0); 48 | }, 49 | // Override dom generator. 50 | getDom: function () { 51 | Log.log("getDom: " + this.name); 52 | let messages = this.messages; 53 | let wrapper = document.createElement("table"); 54 | wrapper.className = this.config.tableClass; 55 | 56 | if (messages.length === 0) { 57 | wrapper.innerHTML = this.loaded ? "No messages" : "Loading Discord connection"; 58 | wrapper.className = this.config.tableClass + " dimmed"; 59 | return wrapper; 60 | } 61 | 62 | if (this.config.fade && this.config.fadePoint < 1) { 63 | if (this.config.fadePoint < 0) { 64 | this.config.fadePoint = 0; 65 | } 66 | var startFade = messages.length * this.config.fadePoint; 67 | var fadeSteps = messages.length - startFade; 68 | } 69 | 70 | let currentFadeStep = 0; 71 | 72 | for (let mi in messages) { 73 | let message = messages[mi]; 74 | console.log(message); 75 | 76 | let mesWrapper = document.createElement("tr"); 77 | mesWrapper.className = "normal"; 78 | if (mi >= startFade) { 79 | currentFadeStep = mi - startFade; 80 | mesWrapper.style.opacity = 1 - (1 / fadeSteps) * currentFadeStep; 81 | } 82 | 83 | //author 84 | let authorWrapper = document.createElement("td"); 85 | authorWrapper.innerHTML = this.titleTransform(message.author, false, this.config.maxAuthorLength, 1); 86 | authorWrapper.className = "author"; 87 | mesWrapper.appendChild(authorWrapper); 88 | 89 | let contentWrapper = document.createElement("td"); 90 | 91 | //message text 92 | let messageDiv = document.createElement("div"); 93 | messageDiv.innerHTML = this.titleTransform(message.text, this.config.wrapEvents, this.config.maxMessageLength, this.config.maxMessageLines); 94 | messageDiv.className = "message" 95 | contentWrapper.appendChild(messageDiv); 96 | 97 | //channel name 98 | if (this.config.showChannel) { 99 | let channel = document.createElement("div"); 100 | channel.innerHTML = this.titleTransform(message.channel, this.config.wrapEvents, this.config.maxMessageLength, this.config.maxMessageLines); 101 | channel.className = "channel" 102 | contentWrapper.appendChild(channel); 103 | } 104 | 105 | mesWrapper.appendChild(contentWrapper); 106 | 107 | //timestamp 108 | var timeWrapper = document.createElement("td"); 109 | timeWrapper.innerHTML = this.capFirst(moment.utc(message.createdAt).fromNow()); //time should be Date from discordjs 110 | timeWrapper.className = "time light"; 111 | mesWrapper.appendChild(timeWrapper); 112 | 113 | wrapper.appendChild(mesWrapper); 114 | } 115 | return wrapper; 116 | }, 117 | /** 118 | * Will send a socket notification to the helper, which will create a Discordjs client with provided api token. 119 | * @param {*} config module config 120 | */ 121 | addDiscord: function (config) { 122 | this.sendSocketNotification("ADD_DISCORD_CONFIG", { 123 | id: this.identifier, 124 | config: config 125 | }); 126 | }, 127 | /** 128 | * I found this in the gitlab MR module, I guess it trims titles to fit a certain size. 129 | * @param {*} string 130 | * @param {*} wrapEvents 131 | * @param {*} maxLength 132 | * @param {*} maxMessageLines 133 | */ 134 | titleTransform: function (string, wrapEvents, maxLength, maxMessageLines) { 135 | if (typeof string !== "string") { 136 | return ""; 137 | } 138 | 139 | if (wrapEvents === true) { 140 | var temp = ""; 141 | var currentLine = ""; 142 | var words = string.split(" "); 143 | var line = 0; 144 | 145 | for (var i = 0; i < words.length; i++) { 146 | var word = words[i]; 147 | if (currentLine.length + word.length < (typeof maxLength === "number" ? maxLength : 25) - 1) { 148 | // max - 1 to account for a space 149 | currentLine += word + " "; 150 | } else { 151 | line++; 152 | if (line > maxMessageLines - 1) { 153 | if (i < words.length) { 154 | currentLine += "…"; 155 | } 156 | break; 157 | } 158 | 159 | if (currentLine.length > 0) { 160 | temp += currentLine + "
" + word + " "; 161 | } else { 162 | temp += word + "
"; 163 | } 164 | currentLine = ""; 165 | } 166 | } 167 | 168 | return (temp + currentLine).trim(); 169 | } else { 170 | if (maxLength && typeof maxLength === "number" && string.length > maxLength) { 171 | return string.trim().slice(0, maxLength) + "…"; 172 | } else { 173 | return string.trim(); 174 | } 175 | } 176 | }, 177 | capFirst: function (string) { 178 | return string.charAt(0).toUpperCase() + string.slice(1); 179 | }, 180 | 181 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | --------------------------------------------------------------------------------