├── LICENSE ├── README.md ├── atom2mastodon.py ├── atom2single.py ├── config.ini ├── rss2mastodon.py └── rss2single.py /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rss2mastodon 2 | 3 | A quick set of python scripts for auto-posting an RSS or Atom feed to Mastodon. Created by AI6YR (Ben) 4 | 5 | ## Mastodon setup 6 | 7 | In order to have this script work, you need the following: 8 | 1. A dedicated Mastodon user account created on a server. 9 | 2. An "access key" for your app created for that user account. (under yourserver/settings/applications) 10 | 11 | ## Python setup 12 | Make sure to install the following Python packages 13 | 14 | `pip3 install bs4 pillow mastodon.py feedparser` 15 | 16 | ## Script setup 17 | 18 | All configuration for the script will ultimately reside in config.ini 19 | 20 | ### Mastodon configuration 21 | * access_token = Mastodon access token 22 | * app_url = Mastodon server 23 | * max_image_size = max image size accepted by server 24 | 25 | ### Feed configuration 26 | * feed_url = URL of the RSS feed you want to query 27 | * feed_name = What you want to name this feed 28 | * feed_visibility = public, unlisted, etc. (per Mastodon.py) 29 | * feed_tags = #your #additional #tags here will be appended to the toot 30 | * feed_delay = delay in seconds between checking on the RSS/Atom feed 31 | * feed_link = whether or not to include the "link" in RSS/Atom in the post 32 | 33 | ## Running the script 34 | 35 | python3 rss2mastodon.py 36 | 37 | or 38 | 39 | python3 atom2mastodon.py 40 | 41 | ## Unattended/background operation 42 | 43 | If you want to run this unattended: 44 | 45 | screen 46 | 47 | nohup python3 -u rss2mastodon.py & 48 | 49 | 50 | -------------------------------------------------------------------------------- /atom2mastodon.py: -------------------------------------------------------------------------------- 1 | """ 2 | RSS -> Fediverse gateway 3 | 4 | AI6YR - Nov 2022 5 | """ 6 | # imports (obviously) 7 | import configparser 8 | from mastodon import Mastodon 9 | import requests 10 | import time 11 | from datetime import datetime 12 | import dateutil 13 | import json 14 | import feedparser 15 | from bs4 import BeautifulSoup 16 | import re 17 | import tempfile 18 | import shutil 19 | from PIL import Image 20 | from PIL import ImageFile 21 | ImageFile.LOAD_TRUNCATED_IMAGES = True 22 | import html 23 | import magic 24 | 25 | # Load the config 26 | config = configparser.ConfigParser() 27 | config.read('config.ini') 28 | 29 | feedurl = config['feed']['feed_url'] 30 | feedname = config['feed']['feed_name'] 31 | feedvisibility = config['feed']['feed_visibility'] 32 | feedtags = config['feed']['feed_tags'] 33 | try: 34 | max_image_size = int(config['mastodon']['max_image_size']) 35 | except: 36 | max_image_size = 1600 37 | try: 38 | feeddelay = int(config['feed']['feed_delay']) 39 | except: 40 | feeddelay = 180 41 | try: 42 | linkfeed = config['feed']['feed_link'].lower() 43 | except: 44 | linkfeed = "false" 45 | try: 46 | usetitle = config['feed']['use_title'].lower() 47 | except: 48 | usetitle = "false" 49 | 50 | print (feedurl) 51 | print (feedname) 52 | # connect to mastodon 53 | mastodonBot = Mastodon( 54 | access_token=config['mastodon']['access_token'], 55 | api_base_url=config['mastodon']['app_url'] 56 | ) 57 | 58 | print ("Starting RSS watcher:" + feedname) 59 | lastpost = "" 60 | maxspottime = 0 61 | lastspottime = datetime.now().timestamp() 62 | while(1): 63 | try: 64 | data = (feedparser.parse(feedurl)) 65 | entries = data["entries"] 66 | for entry in entries: 67 | # print ("----------------") 68 | # print (entry) 69 | link = "" 70 | if (linkfeed == "true"): 71 | try: 72 | link = entry['link'] 73 | except: 74 | pass 75 | if (usetitle == "true"): 76 | clean = re.sub("<.*?>", "", entry['title']) 77 | else: 78 | clean = re.sub("<.*?>", "", entry['summary']) 79 | clean = html.unescape(clean) 80 | clean = clean.replace("&","&") 81 | clean = clean.replace(" nitter.net","https://nitter.net") 82 | clean = clean.replace(" nitter.poast.org","https://nitter.poast.org") 83 | clean = clean.replace(" go.usa.gov","https://go.usa.gov") 84 | clean = clean.replace(" wpc.ncep.noaa.gov","https://wpc.ncep.noaa.gov") 85 | clean = clean.replace(" weather.gov"," https://weather.gov") 86 | clean = clean.replace(" nwschat.weather.gov"," https://nwschat.weather.gov") 87 | clean = clean.replace(" bit.ly"," https://bit.ly") 88 | clean = clean.replace(" owl.ly"," https://owl.ly") 89 | clean = clean.replace(" t.co"," https://t.co") 90 | tootText = clean + feedtags 91 | tootText = clean[:474] + " " + link 92 | spottime = dateutil.parser.parse(entry['published']).timestamp() 93 | if (spottime > maxspottime): 94 | maxspottime = spottime 95 | title = entry['title'] 96 | firsttwo = title[:2] 97 | firstthree = title[:3] 98 | #print("debug: spottime:",spottime) 99 | #print("debug: lastspottime:",lastspottime) 100 | if (spottime > lastspottime): 101 | # if (1): 102 | # print (tootText) 103 | # time.sleep(10) 104 | if (clean == lastpost): 105 | print ("skip: retweet") 106 | elif ("RT" in firsttwo): 107 | print ("skip: retweet") 108 | elif ("Re" in firstthree): 109 | print ("skip: reply") 110 | else: 111 | isposted = False 112 | print (clean) 113 | soup = BeautifulSoup(entry['summary'], 'html.parser') 114 | medialist = [] 115 | for video in soup.findAll('source'): 116 | print("***VIDEO:",video.get('src')) 117 | imgfile = video.get('src') 118 | temp = tempfile.NamedTemporaryFile() 119 | res = requests.get(imgfile, stream = True) 120 | if res.status_code == 200: 121 | shutil.copyfileobj(res.raw, temp) 122 | print('Image sucessfully Downloaded') 123 | print (temp.name) 124 | try: 125 | mediaid = mastodonBot.media_post(temp.name, mime_type="video/mp4") 126 | medialist.append(mediaid) 127 | except Exception as e: 128 | print (e) 129 | print ("Unable to upload video") 130 | else: 131 | print('Video Couldn\'t be retrieved') 132 | temp.close() 133 | for img in soup.findAll('img'): 134 | print("***IMAGE:",img.get('src')) 135 | imgfile = img.get('src') 136 | temp = tempfile.NamedTemporaryFile() 137 | res = requests.get(imgfile, stream = True) 138 | if res.status_code == 200: 139 | shutil.copyfileobj(res.raw, temp) 140 | print('Image sucessfully Downloaded') 141 | print (temp.name) 142 | #ensure image will fit on server 143 | image = Image.open(temp.name) 144 | if ((image.size[0]>max_image_size) or (image.size[1]>max_image_size)): 145 | origx = image.size[0] 146 | origy = image.size[1] 147 | if (origx>origy): 148 | newx = int(max_image_size) 149 | newy = int(origy * (max_image_size/origx)) 150 | else: 151 | newy = int(max_image_size) 152 | newx = int(origx * (max_image_size/origy)) 153 | image = image.resize((newx,newy)) 154 | print ("new image size",image.size) 155 | image.save(temp, format="png") 156 | try: 157 | mediaid = mastodonBot.media_post(temp.name, mime_type="image/png") 158 | medialist.append(mediaid) 159 | except Exception as e: 160 | print ("Unable to upload image.") 161 | print (e) 162 | else: 163 | print('Image Couldn\'t be retrieved') 164 | temp.close() 165 | if (isposted == False): 166 | try: 167 | postedToot = mastodonBot.status_post(tootText,None,medialist,False,feedvisibility) 168 | lastpost = postedToot 169 | except Exception as e: 170 | print(e) 171 | 172 | lastspottime = maxspottime #set last update to the newest update we saw in the list 173 | #print("debug: lastspottime now ",lastspottime) 174 | except Exception as e: 175 | print (e) 176 | time.sleep(feeddelay) 177 | -------------------------------------------------------------------------------- /atom2single.py: -------------------------------------------------------------------------------- 1 | """ 2 | RSS -> Fediverse gateway 3 | Single Shot 4 | 5 | AI6YR - Nov 2022 6 | """ 7 | # imports (obviously) 8 | import configparser 9 | from mastodon import Mastodon 10 | import requests 11 | import time 12 | from datetime import datetime 13 | import dateutil 14 | import json 15 | import feedparser 16 | from bs4 import BeautifulSoup 17 | import re 18 | import tempfile 19 | import shutil 20 | from PIL import Image 21 | import html 22 | import magic 23 | 24 | # Load the config 25 | config = configparser.ConfigParser() 26 | config.read('config.ini') 27 | 28 | feedurl = config['feed']['feed_url'] 29 | feedname = config['feed']['feed_name'] 30 | feedvisibility = config['feed']['feed_visibility'] 31 | feedtags = config['feed']['feed_tags'] 32 | try: 33 | max_image_size = int(config['mastodon']['max_image_size']) 34 | except: 35 | max_image_size = 1600 36 | try: 37 | feeddelay = int(config['feed']['feed_delay']) 38 | except: 39 | feeddelay = 180 40 | try: 41 | linkfeed = config['feed']['feed_link'].lower() 42 | except: 43 | linkfeed = "false" 44 | try: 45 | usetitle = config['feed']['use_title'].lower() 46 | except: 47 | usetitle = "false" 48 | 49 | print (feedurl) 50 | print (feedname) 51 | print (linkfeed) 52 | # connect to mastodon 53 | mastodonBot = Mastodon( 54 | access_token=config['mastodon']['access_token'], 55 | api_base_url=config['mastodon']['app_url'] 56 | ) 57 | 58 | print ("Starting RSS watcher:" + feedname) 59 | lastpost = "" 60 | lastspottime = datetime.now().timestamp() 61 | if (1): 62 | data = (feedparser.parse(feedurl)) 63 | entries = data["entries"] 64 | for entry in entries: 65 | print ("----------------") 66 | # print (entry) 67 | link = "" 68 | if (linkfeed == "true"): 69 | link = entry['link'] 70 | print (link) 71 | if (usetitle == "true"): 72 | clean = re.sub("<.*?>", "", entry['title']) 73 | else: 74 | clean = re.sub("<.*?>", "", entry['summary']) 75 | clean = html.unescape(clean) 76 | clean = clean.replace("&","&") 77 | clean = clean.replace("nitter.net","https://nitter.net") 78 | clean = clean.replace("go.usa.gov","https://go.usa.gov") 79 | clean = clean.replace("wpc.ncep.noaa.gov","https://wpc.ncep.noaa.gov") 80 | clean = clean.replace(" weather.gov"," https://weather.gov") 81 | clean = clean.replace("nwschat.weather.gov"," https://nwschat.weather.gov") 82 | clean = clean.replace(" bit.ly"," https://bit.ly") 83 | clean = clean.replace(" owl.ly"," https://owl.ly") 84 | clean = clean.replace(" t.co"," https://t.co") 85 | tootText = clean + feedtags 86 | tootText = clean[:474] + " " + link 87 | 88 | spottime = dateutil.parser.parse(entry['published']).timestamp() 89 | title = entry['title'] 90 | firsttwo = title[:2] 91 | firstthree = title[:3] 92 | # if (spottime > lastspottime): 93 | if (1): 94 | print (tootText) 95 | value = input ("Toot this? Y/N/Q?") 96 | if (value.lower() == "q"): 97 | quit() 98 | elif (value.lower() == "y"): 99 | print ("Tooting") 100 | isposted = False 101 | print (clean) 102 | soup = BeautifulSoup(entry['summary'], 'html.parser') 103 | medialist = [] 104 | for video in soup.findAll('source'): 105 | print("***VIDEO:",video.get('src')) 106 | imgfile = video.get('src') 107 | temp = tempfile.NamedTemporaryFile() 108 | res = requests.get(imgfile, stream = True) 109 | if res.status_code == 200: 110 | shutil.copyfileobj(res.raw, temp) 111 | print('Image sucessfully Downloaded') 112 | print (temp.name) 113 | mime = magic.Magic(mime=True) 114 | mimetype = mime.from_file(temp.name) 115 | print (mimetype) 116 | mediaid = mastodonBot.media_post(temp.name, mime_type=mimetype) 117 | medialist.append(mediaid) 118 | time.sleep(5) 119 | else: 120 | print('Video Couldn\'t be retrieved') 121 | temp.close() 122 | for img in soup.findAll('img'): 123 | print("***IMAGE:",img.get('src')) 124 | imgfile = img.get('src') 125 | temp = tempfile.NamedTemporaryFile() 126 | res = requests.get(imgfile, stream = True) 127 | if res.status_code == 200: 128 | shutil.copyfileobj(res.raw, temp) 129 | print('Image sucessfully Downloaded') 130 | print (temp.name) 131 | #ensure image will fit on server 132 | image = Image.open(temp.name) 133 | if ((image.size[0]>max_image_size) or (image.size[1]>max_image_size)): 134 | origx = image.size[0] 135 | origy = image.size[1] 136 | if (origx>origy): 137 | newx = int(max_image_size) 138 | newy = int(origy * (max_image_size/origx)) 139 | else: 140 | newy = int(max_image_size) 141 | newx = int(origx * (max_image_size/origy)) 142 | image = image.resize((newx,newy)) 143 | print ("new image size",image.size) 144 | image.save(temp, format="png") 145 | try: 146 | print (temp.name) 147 | mime = magic.Magic(mime=True) 148 | mimetype = mime.from_file(temp.name) 149 | print (mimetype) 150 | #mediaid = mastodonBot.media_post(temp.name, mime_type="image/jpeg") 151 | mediaid = mastodonBot.media_post(temp.name, mime_type=mimetype) 152 | medialist.append(mediaid) 153 | except Exception as e: 154 | print ("unable to upload image") 155 | print (e) 156 | else: 157 | print('Image Couldn\'t be retrieved') 158 | temp.close() 159 | if (isposted == False): 160 | try: 161 | postedToot = mastodonBot.status_post(tootText,None,medialist,False,feedvisibility) 162 | lastpost = postedToot 163 | except Exception as e: 164 | print(e) 165 | 166 | lastspottime = spottime 167 | now = datetime.now().timestamp() 168 | time.sleep(feeddelay) 169 | -------------------------------------------------------------------------------- /config.ini: -------------------------------------------------------------------------------- 1 | [mastodon] 2 | access_token = MYACCESSTOKEN 3 | app_url = MYAPPURL 4 | max_image_size = 1600 5 | [feed] 6 | #feed_url = https://rsshub.app/twitter/user/NWS/ 7 | feed_url = https://nitter.net/NWS/rss 8 | feed_name = National Weather Service 9 | feed_visibility = public 10 | feed_tags = #nws #weather 11 | feed_delay = 300 12 | -------------------------------------------------------------------------------- /rss2mastodon.py: -------------------------------------------------------------------------------- 1 | """ 2 | RSS -> Fediverse gateway 3 | 4 | AI6YR - Nov 2022 5 | """ 6 | # imports (obviously) 7 | import configparser 8 | from mastodon import Mastodon 9 | import requests 10 | import time 11 | from datetime import datetime 12 | import dateutil 13 | import json 14 | import feedparser 15 | from bs4 import BeautifulSoup 16 | import re 17 | import tempfile 18 | import shutil 19 | from PIL import Image 20 | 21 | # Load the config 22 | config = configparser.ConfigParser() 23 | config.read('config.ini') 24 | 25 | feedurl = config['feed']['feed_url'] 26 | feedname = config['feed']['feed_name'] 27 | feedvisibility = config['feed']['feed_visibility'] 28 | feedtags = config['feed']['feed_tags'] 29 | feeddelay = int(config['feed']['feed_delay']) 30 | if (feeddelay < 60): 31 | feeddelay = 300 32 | max_image_size = int(config['mastodon']['max_image_size']) 33 | print (feedurl) 34 | print (feedname) 35 | # connect to mastodon 36 | mastodonBot = Mastodon( 37 | access_token=config['mastodon']['access_token'], 38 | api_base_url=config['mastodon']['app_url'] 39 | ) 40 | 41 | print ("Starting RSS watcher:" + feedname) 42 | lastpost = "" 43 | lastspottime = datetime.now().timestamp() 44 | while(1): 45 | data = (feedparser.parse(feedurl)) 46 | entries = data["entries"] 47 | # print (entries) 48 | for entry in entries: 49 | #print (entry['summary']) 50 | try: 51 | link = entry['link'] 52 | except: 53 | link = "" 54 | clean = re.sub("<.*?>", "", entry['summary']) 55 | clean = clean.replace("&" ,"&") 56 | clean = clean.replace(" " ," ") 57 | spottime = dateutil.parser.parse(entry['published']).timestamp() 58 | firsttwo = clean[:2] 59 | firstthree = clean[:3] 60 | # if (1): 61 | if (spottime > lastspottime): 62 | if (clean == lastpost): 63 | print ("skip: retweet") 64 | elif ("RT" in firsttwo): 65 | print ("skip: retweet") 66 | elif ("Re" in firstthree): 67 | print ("skip: reply") 68 | else: 69 | isposted = False 70 | print (clean) 71 | tootText = clean + feedtags 72 | tootText = tootText[:475] 73 | tootText = tootText + " " + link 74 | soup = BeautifulSoup(entry['summary'], 'html.parser') 75 | medialist = [] 76 | for img in soup.findAll('img'): 77 | print("***IMAGE:",img.get('src')) 78 | imgfile = img.get('src') 79 | temp = tempfile.NamedTemporaryFile() 80 | res = requests.get(imgfile, stream = True) 81 | if res.status_code == 200: 82 | shutil.copyfileobj(res.raw, temp) 83 | print('Image sucessfully Downloaded') 84 | print (temp.name) 85 | image = Image.open(temp.name) 86 | if ((image.size[0]>max_image_size) or (image.size[1]>max_image_size)): 87 | origx = image.size[0] 88 | origy = image.size[1] 89 | if (origx>origy): 90 | newx = int(max_image_size) 91 | newy = int(origy * (max_image_size/origx)) 92 | else: 93 | newy = int(max_image_size) 94 | newx = int(origx * (max_image_size/origy)) 95 | image = image.resize((newx,newy)) 96 | print ("new image size",image.size) 97 | image.save(temp, format="png") 98 | mediaid = mastodonBot.media_post(temp.name, mime_type="image/jpeg") 99 | medialist.append(mediaid) 100 | else: 101 | print('Image Couldn\'t be retrieved') 102 | temp.close() 103 | 104 | try: 105 | postedToot = mastodonBot.status_post(tootText,None,medialist,False,feedvisibility) 106 | lastpost = clean 107 | except Exception as e: 108 | print(e) 109 | 110 | lastspottime = datetime.now().timestamp() 111 | # print ("time:",now) 112 | time.sleep(feeddelay) 113 | -------------------------------------------------------------------------------- /rss2single.py: -------------------------------------------------------------------------------- 1 | """ 2 | RSS -> Fediverse gateway 3 | 4 | rss2single.py allows for selectively pushing RSS entries into Mastodon, one at a time. 5 | 6 | AI6YR Benjamin Kuo - Dec 2023 7 | """ 8 | # imports (obviously) 9 | import configparser 10 | from mastodon import Mastodon 11 | import requests 12 | import time 13 | from datetime import datetime 14 | import dateutil 15 | import json 16 | import feedparser 17 | from bs4 import BeautifulSoup 18 | import re 19 | import tempfile 20 | import shutil 21 | from PIL import Image 22 | 23 | # Load the config 24 | config = configparser.ConfigParser() 25 | config.read('config.ini') 26 | 27 | feedurl = config['feed']['feed_url'] 28 | feedname = config['feed']['feed_name'] 29 | feedvisibility = config['feed']['feed_visibility'] 30 | feedtags = config['feed']['feed_tags'] 31 | max_image_size = int(config['mastodon']['max_image_size']) 32 | print (feedurl) 33 | print (feedname) 34 | # connect to mastodon 35 | mastodonBot = Mastodon( 36 | access_token=config['mastodon']['access_token'], 37 | api_base_url=config['mastodon']['app_url'] 38 | ) 39 | 40 | 41 | print ("Starting RSS watcher:" + feedname) 42 | lastpost = "" 43 | lastspottime = datetime.now().timestamp() 44 | if(1): 45 | data = (feedparser.parse(feedurl)) 46 | entries = data["entries"] 47 | # print (entries) 48 | for entry in entries: 49 | print (entry['summary']) 50 | try: 51 | link = entry['link'] 52 | except: 53 | link = "" 54 | clean = re.sub("<.*?>", "", entry['summary']) 55 | clean = clean.replace("&" ,"&") 56 | clean = clean[:465] + " " + link 57 | 58 | 59 | spottime = dateutil.parser.parse(entry['published']).timestamp() 60 | firsttwo = clean[:2] 61 | firstthree = clean[:3] 62 | # if (1): 63 | print (clean) 64 | value = input ("Toot this? Y/N/Q?") 65 | if (value.lower() == "q"): 66 | quit() 67 | elif (value.lower() == "y"): 68 | print ("Tooting") 69 | isposted = False 70 | print (clean) 71 | tootText = clean + feedtags 72 | tootText = tootText[:499] 73 | soup = BeautifulSoup(entry['summary'], 'html.parser') 74 | medialist = [] 75 | for img in soup.findAll('img'): 76 | print("***IMAGE:",img.get('src')) 77 | imgfile = img.get('src') 78 | temp = tempfile.NamedTemporaryFile() 79 | res = requests.get(imgfile, stream = True) 80 | if res.status_code == 200: 81 | shutil.copyfileobj(res.raw, temp) 82 | print('Image sucessfully Downloaded') 83 | print (temp.name) 84 | image = Image.open(temp.name) 85 | if ((image.size[0]>max_image_size) or (image.size[1]>max_image_size)): 86 | origx = image.size[0] 87 | origy = image.size[1] 88 | if (origx>origy): 89 | newx = int(max_image_size) 90 | newy = int(origy * (max_image_size/origx)) 91 | else: 92 | newy = int(max_image_size) 93 | newx = int(origx * (max_image_size/origy)) 94 | image = image.resize((newx,newy)) 95 | print ("new image size",image.size) 96 | image.save(temp, format="png") 97 | mediaid = mastodonBot.media_post(temp.name, mime_type="image/jpeg") 98 | medialist.append(mediaid) 99 | else: 100 | print('Image Couldn\'t be retrieved') 101 | temp.close() 102 | 103 | try: 104 | postedToot = mastodonBot.status_post(tootText,None,medialist,False,feedvisibility) 105 | lastpost = clean 106 | except Exception as e: 107 | print(e) 108 | 109 | lastspottime = spottime 110 | now = datetime.now().timestamp() 111 | # print ("time:",now) 112 | time.sleep(60) 113 | --------------------------------------------------------------------------------