From d7f4deae18d5aaa4c5d537c44dd3ff5d3e88556e Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Wed, 20 Oct 2021 15:55:59 -0700 Subject: [PATCH 01/13] Rebuilt the bot, moved old files into v1, new py file in main directory and in v2. --- Procfile | 2 +- c | Bin 377 -> 0 bytes neotomabot.py | 264 ++++++------------------ requirements.txt | 8 +- resources/cannedtweets.txt | 18 ++ resources/cannedtwttes.txt | 19 ++ v1/neotomabot.py | 213 +++++++++++++++++++ old_results.json => v1/old_results.json | 0 to_print.json => v1/to_print.json | 0 tweets.json => v1/tweets.json | 0 v2/neotomabot.py | 79 +++++++ 11 files changed, 400 insertions(+), 203 deletions(-) delete mode 100644 c create mode 100644 resources/cannedtweets.txt create mode 100644 resources/cannedtwttes.txt create mode 100644 v1/neotomabot.py rename old_results.json => v1/old_results.json (100%) rename to_print.json => v1/to_print.json (100%) rename tweets.json => v1/tweets.json (100%) create mode 100644 v2/neotomabot.py diff --git a/Procfile b/Procfile index 115328b..6644e1f 100644 --- a/Procfile +++ b/Procfile @@ -1 +1 @@ -worker: python neotomabot.py \ No newline at end of file +worker: python3 neotomabot.py \ No newline at end of file diff --git a/c b/c deleted file mode 100644 index 2d7526568a7ef4308745764059420965b3ae3cab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 377 zcmYk2-A=+l6oj{c#1M(`(!>YghJ;cmB_YOkUH&TNXQ{M}i4hh`YDGr$5|6!8x@|7>R2#i(+S=Re1FJng>U)b< zq1ICaB2O$S_B~nTew 0: + tweet = random.choice(records)['record'] + while tweet['datasetid'] in datasets: + tweet = random.choice(records)['record'] + string = "It's a new {datasettype} dataset from the {databasename} at {sitename}! https://data.neotomadb.org/{datasetid}".format(**tweet) + if len(string) < 280: + api.request('statuses/update', {'status':string}) + datasets.add(tweet['datasetid']) + else: + string = "It's a new dataset from the {databasename} at {sitename}! https://data.neotomadb.org/{datasetid}".format(**tweet) + if len(string) < 280: + api.request('statuses/update', {'status':string}) + datasets.add(tweet['datasetid']) -import os, tweepy, time, sys, json, requests, random, imp, datetime, schedule, time, random - -def twit_auth(): - # Authenticate the twitter session. - # Should only be needed once at the initiation of the code. - - CONSUMER_KEY = os.environ['CONSUMER_KEY'] - CONSUMER_SECRET = os.environ['CONSUMER_SECRET'] - ACCESS_KEY = os.environ['ACCESS_KEY'] - ACCESS_SECRET = os.environ['ACCESS_SECRET'] - - auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) - auth.set_access_token(ACCESS_KEY, ACCESS_SECRET) - api = tweepy.API(auth) - print('Twitter authenticated \n') - return api - - -def check_neotoma(): - # This function call to neotoma, reads a text file, compares the two - # and then outputs all the 'new' records to a different text file. - # Function returns the number of new records returned. - - # inputs: - # 1. text file: old_results.json - # 2. text file: to_print.json - # 3. json call: neotoma - - with open('old_results.json', 'r') as old_file: - old_calls = json.loads(old_file.read()) - - with open('to_print.json', 'r') as print_file: - to_print = json.loads(print_file.read()) - - neotoma = requests.get("http://ceiwin10.cei.psu.edu/NDB/RecentUploads?months=1") - inp_json = json.loads(neotoma.text)['data'] - - def get_datasets(x): - did = [] - for y in x: - did.append(y["DatasetID"]) - return did - - neo_datasets = get_datasets(inp_json) - old_datasets = get_datasets(old_calls) - new_datasets = get_datasets(to_print) - - # So this works - # We now have the numeric dataset IDs for the most recent month of - # new files to neotoma (neo_datasets), all the ones we've already tweeted - # (old_datasets) and all the ones in our queue (new_datasets). - # - # The next thing we want to do is to remove all the neo_datasets that - # are in old_datasets and then remove all the new_datasets that are - # in neo_datasets, append neo_datasets to new_datasets (if new_datasets - # has a length > 0) and then dump new_datasets. - # - # Old datasets gets re-written when the tweets go out. - - # remove all the neo_datasets: - for i in range(len(neo_datasets)-1, 0, -1): - if neo_datasets[i] in old_datasets: - del inp_json[i] - - # This now gives us a pared down version of inp_json - # Now we need to make sure to add any of the to_print to neo_dataset. - # We do this by cycling through new_datasets. Any dataset number that - # is not in old_datasets or neo_datasets gets added to the beginning of - # the new list. This way it is always the first called up when twitter - # posts: - - for i in range(0, len(new_datasets)-1): - if new_datasets[i] not in old_datasets and new_datasets[i] not in neo_datasets: - inp_json.insert(0,to_print[i]) - - # Now write out to file. Old file doesn't get changed until the - # twitter app is run. - with open('to_print.json', 'w') as print_file: - json.dump(inp_json, print_file) - return len(inp_json) - len(to_print) - -def print_neotoma_update(api): - # Check for new records by using the neotoma "recent" API: - old_toprint = check_neotoma() - - # load files: - with open('to_print.json', 'r') as print_file: - to_print = json.loads(print_file.read()) - with open('old_results.json', 'r') as print_file: - old_files = json.loads(print_file.read()) - - print('Neotoma dataset updated.\n') - if (old_toprint) == 1: - # If only a single site has been added: - line = "I've got a backlog of " + str(len(to_print)) + " sites to tweet and " + str(old_toprint) + " site has been added since I last checked Neotoma. http://neotomadb.org" - elif (old_toprint) > 1: - line = "I've got a backlog of " + str(len(to_print)) + " sites to tweet and " + str(old_toprint) + " sites have been added since I last checked Neotoma. http://neotomadb.org" - else: - line = "I've got a backlog of " + str(len(to_print)) + " sites to tweet. Nothing new has been added since I last checked. http://neotomadb.org" - - print('%s' % line) - try: - print('%s' % line) - api.update_status(status=line) - except tweepy.error.TweepError: - print("Twitter error raised") - -def post_tweet(api): - # Read in the printable tweets: - with open('to_print.json', 'r') as print_file: - to_print = json.loads(print_file.read()) - - with open('old_results.json', 'r') as print_file: - old_files = json.loads(print_file.read()) - - print('Files opened\n') - - pr_tw = random.randint(0,len(to_print) - 1) - site = to_print[pr_tw] - - # Get ready to print the first [0] record in to_print: - weblink = 'http://apps.neotomadb.org/Explorer/?datasetid=' + str(site["DatasetID"]) - - # The datasets have long names. I want to match to simplify: - - line = 'Neotoma welcomes ' + site["SiteName"] + ', a ' + site["DatasetType"] + ' dataset by ' + site["Investigator"] + " " + weblink - - # There's a few reasons why the name might be very long, one is the site name, the other is the author name: - if len(line) > 170: - line = 'Neotoma welcomes ' + site["SiteName"] + " by " + site["Investigator"] + " " + weblink - - # If it's still too long then clip the author list: - if len(line) > 170 & site["Investigator"].find(','): - author = site["Investigator"][0:to_print[0]["Investigator"].find(',')] - line = 'Neotoma welcomes ' + site["SiteName"] + " by " + author + " et al. " + weblink - - try: - print('%s' % line) - api.update_status(status=line) - old_files.append(site) - del to_print[pr_tw] - with open('to_print.json', 'w') as print_file: - json.dump(to_print, print_file) - with open('old_results.json', 'w') as print_file: - json.dump(old_files, print_file) - except tweepy.error.TweepError: - print("Twitter error raised") - - -def self_identify(api): - - # Identify myself as the owner of the bot: - line = 'This twitter bot for the Neotoma Paleoecological Database is managed by @sjgoring. Letting you know what\'s new at http://neotomadb.org' - try: - print('%s' % line) - api.update_status(status=line) - except tweepy.error.TweepError: - print("Twitter error raised") def self_identify_hub(api): - # Identify the codebase for the bot: - line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/SimonGoring/neotomabot' - try: - print('%s' % line) - api.update_status(status=line) - except tweepy.error.TweepError: - print("Twitter error raised") - -def other_inf_hub(api): - # Identify the codebase for the bot: - line = ['The bot for the Neotoma Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/SimonGoring/neotomabot', - 'Neotoma has teaching modules you can use in the class room, check it out: https://www.neotomadb.org/education/category/higher_ed/', - 'The governance for Neotoma includes representatives from our constituent databases. Find out more: https://www.neotomadb.org/about/category/governance', - 'We are invested in #cyberinfrastructure. Our response to emerging challenges is posted on @authorea: https://www.authorea.com/users/152134/articles/165940-cyberinfrastructure-in-the-paleosciences-mobilizing-long-tail-data-building-distributed-community-infrastructure-empowering-individual-geoscientists', - 'We keep a list of all publications that have used Neotoma for their research. Want to be added? Contact us! https://www.neotomadb.org/references', - 'These days everyone\'s got a Google Scholar page. So does Neotoma! https://scholar.google.ca/citations?user=idoixqkAAAAJ&hl=en', - 'If you use #rstats then you can access Neotoma data directly thanks to @rOpenSci! https://ropensci.org/tutorials/neotoma_tutorial.html', - 'Neotoma is more than just pollen & mammals; it contains 28 data types incl phytoliths & biochemistry data. Explore! https://www.neotomadb.org/data/category/explorer', - 'Think you\'ve got better tweets? Add them to my code & make a pull request! https://github.com/SimonGoring/neotomabot', - 'Behold, the very first Neotoma dataset, ID 1: https://apps.neotomadb.org/explorer/?datasetid=1', - 'We\'ve got some new R tutorials up online. Is there anything you\'d like to do with Neotoma? http://neotomadb.github.io', - 'Neotoma is a member of the @ICSU_WDS, working to share best practices for data stewardship.', - 'Are you presenting at an upcoming meeting? Will you be talking about Neotoma? Let us know and we can help get the word out! Contact @sjgoring', - 'You know you want to slide into these mentions. . . Let us know what cool #pollen, #paleoecology, #archaeology, #whatever you\'re doing with Neotoma data!', - 'Referencing Neotoma? Why not check out our Quaternary Research paper? https://doi.org/10.1017/qua.2017.105', - 'How is Neotoma leveraging text mining to improve its data holdings? Find out on the @earthcube blog: https://earthcube.wordpress.com/2018/03/06/geodeepdive-into-darkdata/', - "Building an application that could leverage Neotoma data? Our API (https://api-dev.neotomadb.org) is public and open: https://github.com/NeotomaDB/api_nodetest/", - "The landing pages for Neotoma were built using Vue.js, all code is published on Github at https://github.com/NeotomaDB/ndbLandingPage", - "Learn more about how Neotoma makes the most of teaching and cutting-edge research in a new publication in Elements of Paleontology: http://dx.doi.org/10.1017/9781108681582", - "Neotoma is on Slack. Come join the discussion and get involved! We're looking for folks to help with documentation, stewardship and coding. https://join.slack.com/t/neotomadb/shared_invite/zt-cvsv53ep-wjGeCTkq7IhP6eUNA9NxYQ" - ] - - try: - print('%s' % line) - api.update_status(status=line[random.randint(0,len(line))]) - except tweepy.error.TweepError: - print("Twitter error raised") + """ Identify the codebase for the bot through a tweet. """ + line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot' + api.request('statuses/update', {'status':line}) -api = twit_auth() -schedule.every(3).hours.do(post_tweet, api) -schedule.every().day.at("15:37").do(print_neotoma_update, api) -schedule.every().wednesday.at("14:30").do(self_identify, api) +schedule.every(6).hours.do(recentsite, api) +schedule.every(5).hours.do(randomtweet, api) schedule.every().monday.at("14:30").do(self_identify_hub, api) schedule.every().day.at("10:30").do(other_inf_hub, api) diff --git a/requirements.txt b/requirements.txt index 26b386c..a0f9945 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ -tweepy==3.7.0 -requests==2.21.0 -schedule==0.5.0 +schedule==1.1.0 +requests==2.22.0 +xmltodict==0.12.0 +tweepy==4.1.0 +TwitterAPI==2.7.5 diff --git a/resources/cannedtweets.txt b/resources/cannedtweets.txt new file mode 100644 index 0000000..7b8907b --- /dev/null +++ b/resources/cannedtweets.txt @@ -0,0 +1,18 @@ +The bot for the Neotoma Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot +Neotoma has teaching modules you can use in the classroom, check it out: https://www.neotomadb.org/education/category/higher_ed/ +Governance for Neotoma includes representatives from our 34 constituent databases. Find out more: https://www.neotomadb.org/about/category/governance +We are invested in #cyberinfrastructure. Our response to emerging challenges is posted on @authorea: https://www.authorea.com/users/152134/articles/165940-cyberinfrastructure-in-the-paleosciences-mobilizing-long-tail-data-building-distributed-community-infrastructure-empowering-individual-geoscientists +There's a big @zotero library of Neotoma publications that we've been working on. Check it out here: https://www.zotero.org/groups/2321378/neotomadb +Neotoma is more than just pollen & mammals; it contains 28 data types incl phytoliths & biochemistry data. Explore! https://apps.neotomadb.org/explorer +Think you've got better tweets? Add them to my code & make a pull request! https://github.com/NeotomaDB/neotomabot +Behold, the very first Neotoma dataset, ID 1: https://apps.neotomadb.org/explorer/?datasetid=1 +Our site at https://open.neotomadb.org hosts all our #openscience work, including a link to the database schema. Check it out! +Neotoma is a member of the @ICSU_WDS, working to share best practices for data stewardship. +Are you presenting at an upcoming meeting? Will you be talking about Neotoma? Let us know and we can help get the word out! Contact @sjgoring +You know you want to slide into these mentions. . . Let us know what cool #pollen, #paleoecology, #archaeology, #whatever you're doing with Neotoma data! +Referencing Neotoma? Why not check out our Quaternary Research paper? https://doi.org/10.1017/qua.2017.105 +How is Neotoma leveraging text mining to improve its data holdings? We've been working with @geodeepdive to discover articles that have yet to be submitted to the database. @earthcube +Building an application that could leverage Neotoma data? Our API (https://api.neotomadb.org) is public and open: https://github.com/NeotomaDB/api_nodetest/ #openscience +The landing pages for Neotoma were built using Vue.js, all code is published on Github at https://github.com/NeotomaDB/ndbLandingPage Check them out here: https://data.neotomadb.org +Learn more about how Neotoma makes the most of teaching and cutting-edge research in our Elements of Paleontology publication: http://dx.doi.org/10.1017/9781108681582 +Neotoma is on Slack. Come join the discussion and get involved! We're looking for folks to help with documentation, stewardship and coding. https://join.slack.com/t/neotomadb/shared_invite/zt-cvsv53ep-wjGeCTkq7IhP6eUNA9NxYQ \ No newline at end of file diff --git a/resources/cannedtwttes.txt b/resources/cannedtwttes.txt new file mode 100644 index 0000000..e89d4ea --- /dev/null +++ b/resources/cannedtwttes.txt @@ -0,0 +1,19 @@ +The bot for the Neotoma Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot +Neotoma has teaching modules you can use in the class room, check it out: https://www.neotomadb.org/education/category/higher_ed/ +The governance for Neotoma includes representatives from our constituent databases. Find out more: https://www.neotomadb.org/about/category/governance +We are invested in #cyberinfrastructure. Our response to emerging challenges is posted on @authorea: https://www.authorea.com/users/152134/articles/165940-cyberinfrastructure-in-the-paleosciences-mobilizing-long-tail-data-building-distributed-community-infrastructure-empowering-individual-geoscientists +We keep a list of all publications that have used Neotoma for their research. Want to be added? Contact us! https://www.neotomadb.org/references +These days everyone's got a Google Scholar page. So does Neotoma! https://scholar.google.ca/citations?user=idoixqkAAAAJ&hl=en +Neotoma is more than just pollen & mammals; it contains 28 data types incl phytoliths & biochemistry data. Explore! https://apps.neotomadb.org/explorer +Think you've got better tweets? Add them to my code & make a pull request! https://github.com/NeotomaDB/neotomabot +Behold, the very first Neotoma dataset, ID 1: https://apps.neotomadb.org/explorer/?datasetid=1 +Our site at https://open.neotomadb.org hosts all our #openscience work, including a link to the database schema. Check it out! +Neotoma is a member of the @ICSU_WDS, working to share best practices for data stewardship. +Are you presenting at an upcoming meeting? Will you be talking about Neotoma? Let us know and we can help get the word out! Contact @sjgoring +You know you want to slide into these mentions. . . Let us know what cool #pollen, #paleoecology, #archaeology, #whatever you're doing with Neotoma data! +Referencing Neotoma? Why not check out our Quaternary Research paper? https://doi.org/10.1017/qua.2017.105 +How is Neotoma leveraging text mining to improve its data holdings? We've been working with @geodeepdive to discover articles that have yet to be submitted to the database. +Building an application that could leverage Neotoma data? Our API (https://api.neotomadb.org) is public and open: https://github.com/NeotomaDB/api_nodetest/ #openscience +The landing pages for Neotoma were built using Vue.js, all code is published on Github at https://github.com/NeotomaDB/ndbLandingPage Check them out here: https://data.neotomadb.org +Learn more about how Neotoma makes the most of teaching and cutting-edge research in our Elements of Paleontology publication: http://dx.doi.org/10.1017/9781108681582 +Neotoma is on Slack. Come join the discussion and get involved! We're looking for folks to help with documentation, stewardship and coding. https://join.slack.com/t/neotomadb/shared_invite/zt-cvsv53ep-wjGeCTkq7IhP6eUNA9NxYQ \ No newline at end of file diff --git a/v1/neotomabot.py b/v1/neotomabot.py new file mode 100644 index 0000000..8381a62 --- /dev/null +++ b/v1/neotomabot.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +#!python3 + +import os, tweepy, time, sys, json, requests, random, imp, datetime, schedule, time, random + +def twit_auth(): + # Authenticate the twitter session. + # Should only be needed once at the initiation of the code. + + CONSUMER_KEY = os.environ['CONSUMER_KEY'] + CONSUMER_SECRET = os.environ['CONSUMER_SECRET'] + ACCESS_KEY = os.environ['ACCESS_KEY'] + ACCESS_SECRET = os.environ['ACCESS_SECRET'] + + auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET) + auth.set_access_token(ACCESS_KEY, ACCESS_SECRET) + api = tweepy.API(auth) + print('Twitter authenticated \n') + return api + + +def check_neotoma(): + # This function call to neotoma, reads a text file, compares the two + # and then outputs all the 'new' records to a different text file. + # Function returns the number of new records returned. + + # inputs: + # 1. text file: old_results.json + # 2. text file: to_print.json + # 3. json call: neotoma + + with open('old_results.json', 'r') as old_file: + old_calls = json.loads(old_file.read()) + + with open('to_print.json', 'r') as print_file: + to_print = json.loads(print_file.read()) + + neotoma = requests.get("http://ceiwin10.cei.psu.edu/NDB/RecentUploads?months=1") + inp_json = json.loads(neotoma.text)['data'] + + def get_datasets(x): + did = [] + for y in x: + did.append(y["DatasetID"]) + return did + + neo_datasets = get_datasets(inp_json) + old_datasets = get_datasets(old_calls) + new_datasets = get_datasets(to_print) + + # So this works + # We now have the numeric dataset IDs for the most recent month of + # new files to neotoma (neo_datasets), all the ones we've already tweeted + # (old_datasets) and all the ones in our queue (new_datasets). + # + # The next thing we want to do is to remove all the neo_datasets that + # are in old_datasets and then remove all the new_datasets that are + # in neo_datasets, append neo_datasets to new_datasets (if new_datasets + # has a length > 0) and then dump new_datasets. + # + # Old datasets gets re-written when the tweets go out. + + # remove all the neo_datasets: + for i in range(len(neo_datasets)-1, 0, -1): + if neo_datasets[i] in old_datasets: + del inp_json[i] + + # This now gives us a pared down version of inp_json + # Now we need to make sure to add any of the to_print to neo_dataset. + # We do this by cycling through new_datasets. Any dataset number that + # is not in old_datasets or neo_datasets gets added to the beginning of + # the new list. This way it is always the first called up when twitter + # posts: + + for i in range(0, len(new_datasets)-1): + if new_datasets[i] not in old_datasets and new_datasets[i] not in neo_datasets: + inp_json.insert(0,to_print[i]) + + # Now write out to file. Old file doesn't get changed until the + # twitter app is run. + with open('to_print.json', 'w') as print_file: + json.dump(inp_json, print_file) + return len(inp_json) - len(to_print) + +def print_neotoma_update(api): + # Check for new records by using the neotoma "recent" API: + old_toprint = check_neotoma() + + # load files: + with open('to_print.json', 'r') as print_file: + to_print = json.loads(print_file.read()) + with open('old_results.json', 'r') as print_file: + old_files = json.loads(print_file.read()) + + print('Neotoma dataset updated.\n') + if (old_toprint) == 1: + # If only a single site has been added: + line = "I've got a backlog of " + str(len(to_print)) + " sites to tweet and " + str(old_toprint) + " site has been added since I last checked Neotoma. http://neotomadb.org" + elif (old_toprint) > 1: + line = "I've got a backlog of " + str(len(to_print)) + " sites to tweet and " + str(old_toprint) + " sites have been added since I last checked Neotoma. http://neotomadb.org" + else: + line = "I've got a backlog of " + str(len(to_print)) + " sites to tweet. Nothing new has been added since I last checked. http://neotomadb.org" + + print('%s' % line) + try: + print('%s' % line) + api.update_status(status=line) + except tweepy.error.TweepError: + print("Twitter error raised") + +def post_tweet(api): + # Read in the printable tweets: + with open('to_print.json', 'r') as print_file: + to_print = json.loads(print_file.read()) + + with open('old_results.json', 'r') as print_file: + old_files = json.loads(print_file.read()) + + print('Files opened\n') + + pr_tw = random.randint(0,len(to_print) - 1) + site = to_print[pr_tw] + + # Get ready to print the first [0] record in to_print: + weblink = 'http://apps.neotomadb.org/Explorer/?datasetid=' + str(site["DatasetID"]) + + # The datasets have long names. I want to match to simplify: + + line = 'Neotoma welcomes ' + site["SiteName"] + ', a ' + site["DatasetType"] + ' dataset by ' + site["Investigator"] + " " + weblink + + # There's a few reasons why the name might be very long, one is the site name, the other is the author name: + if len(line) > 170: + line = 'Neotoma welcomes ' + site["SiteName"] + " by " + site["Investigator"] + " " + weblink + + # If it's still too long then clip the author list: + if len(line) > 170 & site["Investigator"].find(','): + author = site["Investigator"][0:to_print[0]["Investigator"].find(',')] + line = 'Neotoma welcomes ' + site["SiteName"] + " by " + author + " et al. " + weblink + + try: + print('%s' % line) + api.update_status(status=line) + old_files.append(site) + del to_print[pr_tw] + with open('to_print.json', 'w') as print_file: + json.dump(to_print, print_file) + with open('old_results.json', 'w') as print_file: + json.dump(old_files, print_file) + except tweepy.error.TweepError: + print("Twitter error raised") + + +def self_identify(api): + + # Identify myself as the owner of the bot: + line = 'This twitter bot for the Neotoma Paleoecological Database is managed by @sjgoring. Letting you know what\'s new at http://neotomadb.org' + try: + print('%s' % line) + api.update_status(status=line) + except tweepy.error.TweepError: + print("Twitter error raised") + +def self_identify_hub(api): + # Identify the codebase for the bot: + line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/SimonGoring/neotomabot' + try: + print('%s' % line) + api.update_status(status=line) + except tweepy.error.TweepError: + print("Twitter error raised") + +def other_inf_hub(api): + # Identify the codebase for the bot: + line = ['The bot for the Neotoma Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/SimonGoring/neotomabot', + 'Neotoma has teaching modules you can use in the class room, check it out: https://www.neotomadb.org/education/category/higher_ed/', + 'The governance for Neotoma includes representatives from our constituent databases. Find out more: https://www.neotomadb.org/about/category/governance', + 'We are invested in #cyberinfrastructure. Our response to emerging challenges is posted on @authorea: https://www.authorea.com/users/152134/articles/165940-cyberinfrastructure-in-the-paleosciences-mobilizing-long-tail-data-building-distributed-community-infrastructure-empowering-individual-geoscientists', + 'We keep a list of all publications that have used Neotoma for their research. Want to be added? Contact us! https://www.neotomadb.org/references', + 'These days everyone\'s got a Google Scholar page. So does Neotoma! https://scholar.google.ca/citations?user=idoixqkAAAAJ&hl=en', + 'If you use #rstats then you can access Neotoma data directly thanks to @rOpenSci! https://ropensci.org/tutorials/neotoma_tutorial.html', + 'Neotoma is more than just pollen & mammals; it contains 28 data types incl phytoliths & biochemistry data. Explore! https://www.neotomadb.org/data/category/explorer', + 'Think you\'ve got better tweets? Add them to my code & make a pull request! https://github.com/SimonGoring/neotomabot', + 'Behold, the very first Neotoma dataset, ID 1: https://apps.neotomadb.org/explorer/?datasetid=1', + 'We\'ve got some new R tutorials up online. Is there anything you\'d like to do with Neotoma? http://neotomadb.github.io', + 'Neotoma is a member of the @ICSU_WDS, working to share best practices for data stewardship.', + 'Are you presenting at an upcoming meeting? Will you be talking about Neotoma? Let us know and we can help get the word out! Contact @sjgoring', + 'You know you want to slide into these mentions. . . Let us know what cool #pollen, #paleoecology, #archaeology, #whatever you\'re doing with Neotoma data!', + 'Referencing Neotoma? Why not check out our Quaternary Research paper? https://doi.org/10.1017/qua.2017.105', + 'How is Neotoma leveraging text mining to improve its data holdings? Find out on the @earthcube blog: https://earthcube.wordpress.com/2018/03/06/geodeepdive-into-darkdata/', + "Building an application that could leverage Neotoma data? Our API (https://api-dev.neotomadb.org) is public and open: https://github.com/NeotomaDB/api_nodetest/", + "The landing pages for Neotoma were built using Vue.js, all code is published on Github at https://github.com/NeotomaDB/ndbLandingPage", + "Learn more about how Neotoma makes the most of teaching and cutting-edge research in a new publication in Elements of Paleontology: http://dx.doi.org/10.1017/9781108681582", + "Neotoma is on Slack. Come join the discussion and get involved! We're looking for folks to help with documentation, stewardship and coding. https://join.slack.com/t/neotomadb/shared_invite/zt-cvsv53ep-wjGeCTkq7IhP6eUNA9NxYQ" + ] + + try: + print('%s' % line) + api.update_status(status=line[random.randint(0,len(line))]) + except tweepy.error.TweepError: + print("Twitter error raised") + +api = twit_auth() + +schedule.every(3).hours.do(post_tweet, api) +schedule.every().day.at("15:37").do(print_neotoma_update, api) +schedule.every().wednesday.at("14:30").do(self_identify, api) +schedule.every().monday.at("14:30").do(self_identify_hub, api) +schedule.every().day.at("10:30").do(other_inf_hub, api) + +while 1: + schedule.run_pending() + time.sleep(61) diff --git a/old_results.json b/v1/old_results.json similarity index 100% rename from old_results.json rename to v1/old_results.json diff --git a/to_print.json b/v1/to_print.json similarity index 100% rename from to_print.json rename to v1/to_print.json diff --git a/tweets.json b/v1/tweets.json similarity index 100% rename from tweets.json rename to v1/tweets.json diff --git a/v2/neotomabot.py b/v2/neotomabot.py new file mode 100644 index 0000000..ccb8cb0 --- /dev/null +++ b/v2/neotomabot.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +#!python3 +""" Neotoma Database Twitter Manager + by: Simon Goring + This Twitter bot is intended to provide updated information to individuals about additions to the Neotoma + Paleoecology database. The script leverages the `schedule` package for Python, running continually in + the background, sending out tweets at a specified time and interval. +""" + +from TwitterAPI import TwitterAPI +import random +import xmltodict +import urllib.request +import schedule +import time +import os + +twitstuff = {'consumer_key':os.environ['consumer_key'], + 'consumer_secret': os.environ['consumer_secret'], + 'access_token_key':os.environ['access_token_key'], + 'access_token_secret':os.environ['access_token_secret']} + +datasets = set() + +api = TwitterAPI(consumer_key=twitstuff['consumer_key'], + consumer_secret=twitstuff['consumer_secret'], + access_token_key=twitstuff['access_token_key'], + access_token_secret=twitstuff['access_token_secret']) + +def randomtweet(api): + """ Tweet a random statement from a plain text document. Passing in the twitter API object. + The tweets are all present in the file `resources/cannedtweets.txt`. These can be edited + directly on GitHub if anyone chooses to. + """ + with open('../resources/cannedtweets.txt', 'r') as f: + alltweets = f.read().splitlines() + line = random.choice(alltweets) + api.request('statuses/update', {'status':line}) + +def recentsite(api): + """ Tweet one of the recent data uploads from Neotoma. Passing in the twitter API object. + This leverages the v1.5 API's XML response for recent uploads. It selects one of the new uploads + (except geochronology uploads) and tweets it out. It selects them randomly, and adds the selected + dataset to a set object so that values cannot be repeatedly tweeted out. + """ + with urllib.request.urlopen('https://api.neotomadb.org/v1.5/data/recentuploads/1') as response: + html = response.read() + output = xmltodict.parse(html)['results']['results'] + records = list(filter(lambda x: x['record']['datasettype'] != 'geochronology' or x['record']['datasetid'] not in datasets, output)) + if len(records) > 0: + tweet = random.choice(records)['record'] + while tweet['datasetid'] in datasets: + tweet = random.choice(records)['record'] + string = "It's a new {datasettype} dataset from the {databasename} at {sitename}! https://data.neotomadb.org/{datasetid}".format(**tweet) + if len(string) < 280: + api.request('statuses/update', {'status':string}) + datasets.add(tweet['datasetid']) + else: + string = "It's a new dataset from the {databasename} at {sitename}! https://data.neotomadb.org/{datasetid}".format(**tweet) + if len(string) < 280: + api.request('statuses/update', {'status':string}) + datasets.add(tweet['datasetid']) + + +def self_identify_hub(api): + """ Identify the codebase for the bot through a tweet. """ + line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot' + api.request('statuses/update', {'status':line}) + + +schedule.every(6).hours.do(recentsite, api) +schedule.every(5).hours.do(randomtweet, api) +schedule.every().monday.at("14:30").do(self_identify_hub, api) +schedule.every().day.at("10:30").do(other_inf_hub, api) + +while 1: + schedule.run_pending() + time.sleep(61) From a973b66ef79e4094d35656fc8033e08a2c193a53 Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Wed, 20 Oct 2021 16:02:00 -0700 Subject: [PATCH 02/13] Adding the first tweet out, to make sure things are working. --- neotomabot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neotomabot.py b/neotomabot.py index d116990..6c27b2d 100644 --- a/neotomabot.py +++ b/neotomabot.py @@ -28,6 +28,10 @@ access_token_key=twitstuff['access_token_key'], access_token_secret=twitstuff['access_token_secret']) +def twitterup(api): + line = "Someone just restarted me by pushing to GitHub. This means I've been updated, yay!" + api.request('statuses/update', {'status':line}) + def randomtweet(api): """ Tweet a random statement from a plain text document. Passing in the twitter API object. The tweets are all present in the file `resources/cannedtweets.txt`. These can be edited @@ -68,6 +72,7 @@ def self_identify_hub(api): line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot' api.request('statuses/update', {'status':line}) +twitterup(api) schedule.every(6).hours.do(recentsite, api) schedule.every(5).hours.do(randomtweet, api) From 55be41cfb2342efdcc35650c03d616587029eeb4 Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Wed, 20 Oct 2021 16:39:56 -0700 Subject: [PATCH 03/13] Added a code of conduct and updated the README. Fixed an error in the code. --- CODE_OF_CONDUCT.md | 25 ++++++++++++++++++++ README.md | 44 +++++++++++++++++++++-------------- neotomabot.py | 1 - resources/cannedtwttes.txt | 19 --------------- resources/neotomatwitter.png | Bin 0 -> 60069 bytes 5 files changed, 51 insertions(+), 38 deletions(-) create mode 100644 CODE_OF_CONDUCT.md delete mode 100644 resources/cannedtwttes.txt create mode 100644 resources/neotomatwitter.png diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..0379038 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,25 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, we pledge to respect all people who +contribute through reporting issues, posting feature requests, updating documentation, +submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual language or +imagery, derogatory comments or personal attacks, trolling, public or private harassment, +insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, +commits, code, wiki edits, issues, and other contributions that are not aligned to this +Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed +from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the Contributor Covenant +(http://contributor-covenant.org), version 1.0.0, available at +http://contributor-covenant.org/version/1/0/0/ diff --git a/README.md b/README.md index 7902b71..260522e 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,38 @@ -NeotomaBot -========== +# NeotomaBot -by: Simon Goring +[![Lifecycle:Stable](https://img.shields.io/badge/Lifecycle-Stable-97ca00)](https://neotomadb.org) +[![NSF-1948926](https://img.shields.io/badge/NSF-1948926-blue.svg)](https://nsf.gov/awardsearch/showAward?AWD_ID=1948926) -May 4, 2015 - -Description ----------------------- A twitter bot to search for new records in the [Neotoma Paleoecology Database](http://neotomadb.org) and then post them to the [@neotomadb](http://twitter.com/neotomadb) Twitter account. This program was an experiment to see how good my Python programming skills are. Apparently they're okay. The code could probably use some cleaning, but I'm generally happy with the way it turned out. The program runs on a free [Heroku](https://heroku.com) dyno and tweets semi-regularly. -Requirements ------------------------------ -The program uses `tweepy`, `time`, `sys`, `json`, `requests`, `random` and `imp` package for Python, as well as the Neotoma [API](http://api.neotomadb.org/doc/about). It was coded in Notepad++ because I wanted to try to do it quickly. +![Neotoma Twitter Banner](resources/neotomatwitter.png) +## Contributors + +This project is an open project, and contributions are welcome from any individual. All contributors to this project are bound by a [code of conduct](CODE_OF_CONDUCT.md). Please review and follow this code of conduct as part of your contribution. + +* [Simon Goring](http://goring.org) [![orcid](https://img.shields.io/badge/orcid-0000--0002--2700--4605-brightgreen.svg)](https://orcid.org/0000-0002-2700-4605) + +### Tips for Contributing + +Issues and bug reports are always welcome. Code clean-up, and feature additions can be done either through pull requests to project forks or branches. + +## Requirements + +This application runs using Python v3. All required packages are listed in the [requirements.txt](requirements.txt) file, generated using the python package `pipreqs`. + +Tweets are pulled either from the [resources/cannedtweets.txt](resources/cannedtweets.txt) or generated using the [Neotoma API](https://api.neotomadb.org) `/v1.5/data/recentuploads/n` endpoint. This endpoint returns a list of all dataset uploads within the last `n` months using an XML format. -The OAuth information is hidden on my computer and added to the `.gitignore`. If you want to run this yourself you'll need to go to the Twitter [apps](https://apps.twitter.com/) page and register a bot of your own. Once you get the `CONSUMER_KEY` and the other associated KEYs and SECRETs put them in a file called `apikeys.txt`. The code will work. +The application requires four environment variables, stored as configuration variables in the Heroku dynamo. These are the keys required to access the Twitter API. To obtain these keys for your own use you must add the [Developer status](https://developer.twitter.com) to your existing Twitter account and register an application to obtain the following keys: -Contributions and Bugs ----------------- -This is a work in progress. I'd like to add some more functionality in the near future, for example, following and reposting any posts using the hashtag [`#neotomadb`](https://twitter.com/search?f=realtime&q=%23neotomadb), and posting links to articles using the Neotoma Database. +* CONSUMER_KEY +* CONSUMER_SECRET +* ACCESS_TOKEN_KEY +* ACCESS_TOKEN_SECRET -If you have any issues (about the program!), would like to fork the repository, or would like to help improve or add functionality please feel free to contribute. +### Running Locally -License -------------------- -This project is distributed under an [MIT](http://opensource.org/licenses/MIT) license. +It is possible to run this code locally if you have the environment variables set. Just run `python3 neotomabot.py` \ No newline at end of file diff --git a/neotomabot.py b/neotomabot.py index 6c27b2d..15cf3f5 100644 --- a/neotomabot.py +++ b/neotomabot.py @@ -77,7 +77,6 @@ def self_identify_hub(api): schedule.every(6).hours.do(recentsite, api) schedule.every(5).hours.do(randomtweet, api) schedule.every().monday.at("14:30").do(self_identify_hub, api) -schedule.every().day.at("10:30").do(other_inf_hub, api) while 1: schedule.run_pending() diff --git a/resources/cannedtwttes.txt b/resources/cannedtwttes.txt deleted file mode 100644 index e89d4ea..0000000 --- a/resources/cannedtwttes.txt +++ /dev/null @@ -1,19 +0,0 @@ -The bot for the Neotoma Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot -Neotoma has teaching modules you can use in the class room, check it out: https://www.neotomadb.org/education/category/higher_ed/ -The governance for Neotoma includes representatives from our constituent databases. Find out more: https://www.neotomadb.org/about/category/governance -We are invested in #cyberinfrastructure. Our response to emerging challenges is posted on @authorea: https://www.authorea.com/users/152134/articles/165940-cyberinfrastructure-in-the-paleosciences-mobilizing-long-tail-data-building-distributed-community-infrastructure-empowering-individual-geoscientists -We keep a list of all publications that have used Neotoma for their research. Want to be added? Contact us! https://www.neotomadb.org/references -These days everyone's got a Google Scholar page. So does Neotoma! https://scholar.google.ca/citations?user=idoixqkAAAAJ&hl=en -Neotoma is more than just pollen & mammals; it contains 28 data types incl phytoliths & biochemistry data. Explore! https://apps.neotomadb.org/explorer -Think you've got better tweets? Add them to my code & make a pull request! https://github.com/NeotomaDB/neotomabot -Behold, the very first Neotoma dataset, ID 1: https://apps.neotomadb.org/explorer/?datasetid=1 -Our site at https://open.neotomadb.org hosts all our #openscience work, including a link to the database schema. Check it out! -Neotoma is a member of the @ICSU_WDS, working to share best practices for data stewardship. -Are you presenting at an upcoming meeting? Will you be talking about Neotoma? Let us know and we can help get the word out! Contact @sjgoring -You know you want to slide into these mentions. . . Let us know what cool #pollen, #paleoecology, #archaeology, #whatever you're doing with Neotoma data! -Referencing Neotoma? Why not check out our Quaternary Research paper? https://doi.org/10.1017/qua.2017.105 -How is Neotoma leveraging text mining to improve its data holdings? We've been working with @geodeepdive to discover articles that have yet to be submitted to the database. -Building an application that could leverage Neotoma data? Our API (https://api.neotomadb.org) is public and open: https://github.com/NeotomaDB/api_nodetest/ #openscience -The landing pages for Neotoma were built using Vue.js, all code is published on Github at https://github.com/NeotomaDB/ndbLandingPage Check them out here: https://data.neotomadb.org -Learn more about how Neotoma makes the most of teaching and cutting-edge research in our Elements of Paleontology publication: http://dx.doi.org/10.1017/9781108681582 -Neotoma is on Slack. Come join the discussion and get involved! We're looking for folks to help with documentation, stewardship and coding. https://join.slack.com/t/neotomadb/shared_invite/zt-cvsv53ep-wjGeCTkq7IhP6eUNA9NxYQ \ No newline at end of file diff --git a/resources/neotomatwitter.png b/resources/neotomatwitter.png new file mode 100644 index 0000000000000000000000000000000000000000..e6474e7644256fbcde0dfa2f0c13913c847d6f8e GIT binary patch literal 60069 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 L&1a=g&4d06xeHJ_ literal 0 HcmV?d00001 From a315a8636ce328d19daee9a4d1991d4c64f56cfd Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Wed, 20 Oct 2021 17:01:14 -0700 Subject: [PATCH 04/13] Removed jupyter files, not sure where they came from. Added Github citation file. --- .../NeotomaTwitterBot-checkpoint.ipynb | 232 ------------------ CITATION.cff | 10 + 2 files changed, 10 insertions(+), 232 deletions(-) delete mode 100644 .ipynb_checkpoints/NeotomaTwitterBot-checkpoint.ipynb create mode 100644 CITATION.cff diff --git a/.ipynb_checkpoints/NeotomaTwitterBot-checkpoint.ipynb b/.ipynb_checkpoints/NeotomaTwitterBot-checkpoint.ipynb deleted file mode 100644 index 1828e5d..0000000 --- a/.ipynb_checkpoints/NeotomaTwitterBot-checkpoint.ipynb +++ /dev/null @@ -1,232 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 27, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Twitter authenticated \n", - "\n", - "Files opened\n", - "\n", - "Neotoma welcomes another dataset: Greenbrier Lake from A.J. Smith, D.F. Palmer http://apps.neotomadb.org/Explorer/?datasetid=15823\n" - ] - }, - { - "ename": "TweepError", - "evalue": "Twitter error response: status code = 403", - "output_type": "error", - "traceback": [ - "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[1;31mTweepError\u001b[0m Traceback (most recent call last)", - "\u001b[1;32m\u001b[0m in \u001b[0;36m\u001b[1;34m()\u001b[0m\n\u001b[0;32m 119\u001b[0m \u001b[0mcheck_neotoma\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 120\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 121\u001b[1;33m \u001b[0mpost_tweet\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[1;32m\u001b[0m in \u001b[0;36mpost_tweet\u001b[1;34m()\u001b[0m\n\u001b[0;32m 101\u001b[0m \u001b[0mapi\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate_status\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mstatus\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mline\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 102\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 103\u001b[1;33m \u001b[0mapi\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mupdate_status\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mstatus\u001b[0m\u001b[1;33m=\u001b[0m\u001b[0mline\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 104\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 105\u001b[0m \u001b[1;31m# Add the tweeted site to `old_files` and then delete it from the to_print.\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32mC:\\Python34\\lib\\site-packages\\tweepy\\api.py\u001b[0m in \u001b[0;36mupdate_status\u001b[1;34m(self, media_ids, *args, **kwargs)\u001b[0m\n\u001b[0;32m 191\u001b[0m \u001b[0mallowed_param\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;33m[\u001b[0m\u001b[1;34m'status'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'in_reply_to_status_id'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'lat'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'long'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'source'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'place_id'\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;34m'display_coordinates'\u001b[0m\u001b[1;33m]\u001b[0m\u001b[1;33m,\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 192\u001b[0m \u001b[0mrequire_auth\u001b[0m\u001b[1;33m=\u001b[0m\u001b[1;32mTrue\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 193\u001b[1;33m )(post_data=post_data, *args, **kwargs)\n\u001b[0m\u001b[0;32m 194\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 195\u001b[0m \u001b[1;32mdef\u001b[0m \u001b[0mmedia_upload\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mself\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mfilename\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m*\u001b[0m\u001b[0margs\u001b[0m\u001b[1;33m,\u001b[0m \u001b[1;33m**\u001b[0m\u001b[0mkwargs\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32mC:\\Python34\\lib\\site-packages\\tweepy\\binder.py\u001b[0m in \u001b[0;36m_call\u001b[1;34m(*args, **kwargs)\u001b[0m\n\u001b[0;32m 237\u001b[0m \u001b[1;32mreturn\u001b[0m \u001b[0mmethod\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 238\u001b[0m \u001b[1;32melse\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 239\u001b[1;33m \u001b[1;32mreturn\u001b[0m \u001b[0mmethod\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mexecute\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 240\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 241\u001b[0m \u001b[1;31m# Set pagination mode\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;32mC:\\Python34\\lib\\site-packages\\tweepy\\binder.py\u001b[0m in \u001b[0;36mexecute\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m 221\u001b[0m \u001b[1;32mexcept\u001b[0m \u001b[0mException\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 222\u001b[0m \u001b[0merror_msg\u001b[0m \u001b[1;33m=\u001b[0m \u001b[1;34m\"Twitter error response: status code = %s\"\u001b[0m \u001b[1;33m%\u001b[0m \u001b[0mresp\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mstatus_code\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 223\u001b[1;33m \u001b[1;32mraise\u001b[0m \u001b[0mTweepError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0merror_msg\u001b[0m\u001b[1;33m,\u001b[0m \u001b[0mresp\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m 224\u001b[0m \u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m 225\u001b[0m \u001b[1;31m# Parse the response payload\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n", - "\u001b[1;31mTweepError\u001b[0m: Twitter error response: status code = 403" - ] - } - ], - "source": [ - "#!/usr/bin/env python\n", - "# -*- coding: utf-8 -*-\n", - "#!python3\n", - "\n", - "import tweepy, time, sys, json, requests, random\n", - " \n", - "def check_neotoma():\n", - " ## This function call to neotoma, reads a text file, compares the two\n", - " ## and then returns all the 'new' records to a different text file.\n", - "\n", - " # inputs:\n", - " # 1. text file: old_results.json\n", - " # 2. text file: to_print.json\n", - " # 3. json call: neotoma\n", - "\n", - " with open('old_results.json', 'r') as old_file:\n", - " old_calls = json.loads(old_file.read())\n", - " \n", - " with open('to_print.json', 'r') as print_file:\n", - " to_print = json.loads(print_file.read())\n", - " \n", - " neotoma = requests.get(\"http://ceiwin10.cei.psu.edu/NDB/RecentUploads?months=1\")\n", - " inp_json = json.loads(neotoma.text)['data']\n", - "\n", - " def get_datasets(x):\n", - " did = []\n", - " for y in x:\n", - " did.append(y[\"DatasetID\"])\n", - " return did\n", - "\n", - " neo_datasets = get_datasets(inp_json)\n", - " old_datasets = get_datasets(old_calls)\n", - " new_datasets = get_datasets(to_print)\n", - " \n", - " # So this works\n", - " # We now have the numeric dataset IDs for the most recent month of\n", - " # new files to neotoma (neo_datasets), all the ones we've already tweeted\n", - " # (old_datasets) and all the ones in our queue (new_datasets).\n", - " #\n", - " # The next thing we want to do is to remove all the neo_datasets that\n", - " # are in old_datasets and then remove all the new_datasets that are\n", - " # in neo_datasets, append neo_datasets to new_datasets (if new_datasets\n", - " # has a length > 0) and then dump new_datasets.\n", - " #\n", - " # Old datasets gets re-written when the tweets go out.\n", - "\n", - " # remove all the neo_datasets:\n", - " for i in range(len(neo_datasets)-1, 0, -1):\n", - " if neo_datasets[i] in old_datasets:\n", - " del inp_json[i]\n", - "\n", - " # This now gives us a pared down version of inp_json\n", - " # Now we need to make sure to add any of the to_print to neo_dataset.\n", - " # We do this by cycling through new_datasets. Any dataset number that\n", - " # is not in old_datasets or neo_datasets gets added to the beginning of\n", - " # the new list. This way it is always the first called up when twitter\n", - " # posts:\n", - " \n", - " for i in range(0, len(new_datasets)-1):\n", - " if new_datasets[i] not in old_datasets and new_datasets[i] not in neo_datasets:\n", - " inp_json.insert(0,to_print[i])\n", - "\n", - " # Now write out to file. Old file doesn't get changed until the\n", - " # twitter app is run.\n", - " with open('to_print.json', 'w') as print_file:\n", - " json.dump(inp_json, print_file)\n", - "\n", - "def post_tweet():\n", - " CONSUMER_KEY = 'jou6H9DZLPzw6f3aSIY7wzC6n'\n", - " CONSUMER_SECRET = 'eum3NCrtrVC1tFsGvEj0GuqsxwQCWFfN8nmgcbMyA5xdmQhSdU'\n", - " ACCESS_KEY = '3184480124-AHNgg72lXKYEuOjyzh5WKzBMkBBejpKIX9OxKpX'\n", - " ACCESS_SECRET = 'GAmE6PX3ulj61tluwXA6jUKcPJwoCNToCg5JrJS8BbA3U'\n", - " auth = tweepy.OAuthHandler(CONSUMER_KEY, CONSUMER_SECRET)\n", - " auth.set_access_token(ACCESS_KEY, ACCESS_SECRET)\n", - " api = tweepy.API(auth)\n", - "\n", - " print('Twitter authenticated \\n')\n", - " \n", - " # Read in the printable tweets:\n", - " with open('to_print.json', 'r') as print_file:\n", - " to_print = json.loads(print_file.read())\n", - " \n", - " with open('old_results.json', 'r') as print_file:\n", - " old_files = json.loads(print_file.read())\n", - " \n", - " print('Files opened\\n')\n", - " \n", - " # Now loop through the records:\n", - " while len(to_print) > 0:\n", - " weblink = 'http://apps.neotomadb.org/Explorer/?datasetid=' + str(to_print[0][\"DatasetID\"])\n", - " \n", - " line = 'Neotoma welcomes another ' + to_print[0][\"DatabaseName\"] + ' dataset: ' + to_print[0][\"SiteName\"] + \" from \" + to_print[0][\"Investigator\"] + \" \" + weblink\n", - " \n", - " if len(line) > 170:\n", - " line = 'Neotoma welcomes another dataset: ' + to_print[0][\"SiteName\"] + \" from \" + to_print[0][\"Investigator\"] + \" \" + weblink\n", - " \n", - " print('%s' % line)\n", - " \n", - " if random.randint(0,30) == 10:\n", - " line = 'This is a twitter bot for the Neotoma Paleoecological Database, letting you know what\\'s new. http://neotomadb.org managed by @sjgoring'\n", - " api.update_status(status=line)\n", - " else:\n", - " api.update_status(status=line)\n", - "\n", - " # Add the tweeted site to `old_files` and then delete it from the to_print.\n", - " old_files.append(to_print[0])\n", - "\n", - " del to_print[0]\n", - "\n", - " with open('to_print.json', 'w') as print_file:\n", - " json.dump(to_print, print_file)\n", - "\n", - " with open('old_results.json', 'w') as print_file:\n", - " json.dump(old_files, print_file)\n", - "\n", - " time.sleep(600) # Tweet every 10 minutes.\n", - " \n", - " if len(to_print) < 5:\n", - " check_neotoma()\n", - "\n", - "post_tweet()" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [ - "import tweepy, time, sys, json, requests, random" - ] - }, - { - "cell_type": "code", - "execution_count": 24, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "3" - ] - }, - "execution_count": 24, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "collapsed": true - }, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.4.1" - } - }, - "nbformat": 4, - "nbformat_minor": 0 -} diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..09be05a --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,10 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below." +authors: +- family-names: "Goring" + given-names: "Simon" + orcid: "https://orcid.org/0000-0002-2700-4605" +title: "Neotoma Twitter Bot" +version: 2.0 +date-released: 2021-10-20 +url: "https://github.com/NeotomaDB/NeotomaBot" \ No newline at end of file From 3e38430e9fdb5793c25acc0f8fe07e87c566e3f8 Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Wed, 20 Oct 2021 17:21:46 -0700 Subject: [PATCH 05/13] Adding DOI & badge. --- CITATION.cff | 1 + README.md | 1 + 2 files changed, 2 insertions(+) diff --git a/CITATION.cff b/CITATION.cff index 09be05a..13dabe1 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -7,4 +7,5 @@ authors: title: "Neotoma Twitter Bot" version: 2.0 date-released: 2021-10-20 +doi: 10.5281/zenodo.5587213 url: "https://github.com/NeotomaDB/NeotomaBot" \ No newline at end of file diff --git a/README.md b/README.md index 260522e..750abea 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # NeotomaBot [![Lifecycle:Stable](https://img.shields.io/badge/Lifecycle-Stable-97ca00)](https://neotomadb.org) +[![DOI](https://zenodo.org/badge/417625973.svg)](https://zenodo.org/badge/latestdoi/417625973) [![NSF-1948926](https://img.shields.io/badge/NSF-1948926-blue.svg)](https://nsf.gov/awardsearch/showAward?AWD_ID=1948926) A twitter bot to search for new records in the [Neotoma Paleoecology Database](http://neotomadb.org) and then post them to the [@neotomadb](http://twitter.com/neotomadb) Twitter account. From c75ea40ea53114e0721a90f498ecd1b5e84c7290 Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Mon, 25 Oct 2021 09:15:04 -0700 Subject: [PATCH 06/13] Adding new tweets --- resources/cannedtweets.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/resources/cannedtweets.txt b/resources/cannedtweets.txt index 7b8907b..851f18c 100644 --- a/resources/cannedtweets.txt +++ b/resources/cannedtweets.txt @@ -1,6 +1,10 @@ The bot for the Neotoma Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot +We've been working with the @EuPolDB to update records and get new data in. Looking forward to lots more #openscience! http://www.europeanpollendatabase.net/index.php +Such amazing work from our partners at the Latin American Pollen Database! Glad to have more records from this important region https://www.latinamericapollendb.com/ Neotoma has teaching modules you can use in the classroom, check it out: https://www.neotomadb.org/education/category/higher_ed/ Governance for Neotoma includes representatives from our 34 constituent databases. Find out more: https://www.neotomadb.org/about/category/governance +Collaboration with @carletonserc led to the development of a number of #paleoecology instruction modules from high school to upper college courses: https://serc.carleton.edu/neotoma/activities.html +Honestly, there's so much to be done with Neotoma, let us know if you're looking for a project to work on. We'd be happy to help! We are invested in #cyberinfrastructure. Our response to emerging challenges is posted on @authorea: https://www.authorea.com/users/152134/articles/165940-cyberinfrastructure-in-the-paleosciences-mobilizing-long-tail-data-building-distributed-community-infrastructure-empowering-individual-geoscientists There's a big @zotero library of Neotoma publications that we've been working on. Check it out here: https://www.zotero.org/groups/2321378/neotomadb Neotoma is more than just pollen & mammals; it contains 28 data types incl phytoliths & biochemistry data. Explore! https://apps.neotomadb.org/explorer @@ -15,4 +19,6 @@ How is Neotoma leveraging text mining to improve its data holdings? We've been w Building an application that could leverage Neotoma data? Our API (https://api.neotomadb.org) is public and open: https://github.com/NeotomaDB/api_nodetest/ #openscience The landing pages for Neotoma were built using Vue.js, all code is published on Github at https://github.com/NeotomaDB/ndbLandingPage Check them out here: https://data.neotomadb.org Learn more about how Neotoma makes the most of teaching and cutting-edge research in our Elements of Paleontology publication: http://dx.doi.org/10.1017/9781108681582 -Neotoma is on Slack. Come join the discussion and get involved! We're looking for folks to help with documentation, stewardship and coding. https://join.slack.com/t/neotomadb/shared_invite/zt-cvsv53ep-wjGeCTkq7IhP6eUNA9NxYQ \ No newline at end of file +Neotoma is on Slack. Come join the discussion and get involved! We're looking for folks to help with documentation, stewardship and coding. https://join.slack.com/t/neotomadb/shared_invite/zt-cvsv53ep-wjGeCTkq7IhP6eUNA9NxYQ +Neotoma is at the center of research, engagement and outreach. Find out more in our Elements of Paleontology article: https://doi.org/10.1017/9781108681582 +Do you like Diatoms? We're working with @AcadNatSci to get more diatom and water chemistry data into Neotoma. Look how pretty these things are! https://artsandculture.google.com/exhibit/diatoms-of-the-academy-of-natural-sciences-of-drexel-university-academy-of-natural-sciences-of-drexel-university/7QKi7EaVlShRLw?hl=en From 0b02edbc6c0aeb5a98f6b84350deb016372c0334 Mon Sep 17 00:00:00 2001 From: SimonGoring Date: Sat, 26 Feb 2022 10:58:52 -0800 Subject: [PATCH 07/13] Modifications to support new tweet class. --- neotomabot.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/neotomabot.py b/neotomabot.py index 15cf3f5..0897432 100644 --- a/neotomabot.py +++ b/neotomabot.py @@ -10,6 +10,8 @@ from TwitterAPI import TwitterAPI import random +import requests +import json import xmltodict import urllib.request import schedule @@ -56,26 +58,39 @@ def recentsite(api): tweet = random.choice(records)['record'] while tweet['datasetid'] in datasets: tweet = random.choice(records)['record'] - string = "It's a new {datasettype} dataset from the {databasename} at {sitename}! https://data.neotomadb.org/{datasetid}".format(**tweet) + string = "It's a new {datasettype} dataset from the {databasename} at {sitename} ({geo})! https://data.neotomadb.org/{datasetid}".format(**tweet) if len(string) < 280: api.request('statuses/update', {'status':string}) datasets.add(tweet['datasetid']) else: - string = "It's a new dataset from the {databasename} at {sitename}! https://data.neotomadb.org/{datasetid}".format(**tweet) + string = "It's a new dataset from the {databasename} at {sitename} ({geo})! https://data.neotomadb.org/{datasetid}".format(**tweet) if len(string) < 280: api.request('statuses/update', {'status':string}) datasets.add(tweet['datasetid']) +def ukrsite(api): + """ Tweet one of the recent data uploads from Neotoma. Passing in the twitter API object. + This leverages the v1.5 API's XML response for recent uploads. It selects one of the new uploads + (except geochronology uploads) and tweets it out. It selects them randomly, and adds the selected + dataset to a set object so that values cannot be repeatedly tweeted out. + """ + with requests.get('https://api.neotomadb.org/v2.0/data/geopoliticalunits/5852/datasets?limit=9000') as response: + output = filter(lambda x: x["geopoliticalname"] == "Ukraine", json.loads(response.text)['data']) + records = list(map(lambda x: {'id': x['siteid'], 'name': x['sitename']}, list(output)[0]['sites'])) + if len(records) > 0: + tweet = random.choice(records) + string = "{name} is a site in Neotoma from the Ukraine πŸ‡ΊπŸ‡¦ https://apps.neotomadb.org/?siteids={id}".format(**tweet) + api.request('statuses/update', {'status':string}) + def self_identify_hub(api): """ Identify the codebase for the bot through a tweet. """ line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot' api.request('statuses/update', {'status':line}) -twitterup(api) - schedule.every(6).hours.do(recentsite, api) schedule.every(5).hours.do(randomtweet, api) +schedule.every(1).hours.do(ukrsite, api) schedule.every().monday.at("14:30").do(self_identify_hub, api) while 1: From a19d86aab9426c1e70bc61ef0c1242a7f77523d6 Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Sat, 26 Feb 2022 11:18:23 -0800 Subject: [PATCH 08/13] Fixing commandline errors --- neotomabot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/neotomabot.py b/neotomabot.py index 0897432..0526cff 100644 --- a/neotomabot.py +++ b/neotomabot.py @@ -18,7 +18,7 @@ import time import os -twitstuff = {'consumer_key':os.environ['consumer_key'], +twitstuff = {'consumer_key': os.environ['consumer_key'], 'consumer_secret': os.environ['consumer_secret'], 'access_token_key':os.environ['access_token_key'], 'access_token_secret':os.environ['access_token_secret']} @@ -34,6 +34,7 @@ def twitterup(api): line = "Someone just restarted me by pushing to GitHub. This means I've been updated, yay!" api.request('statuses/update', {'status':line}) + def randomtweet(api): """ Tweet a random statement from a plain text document. Passing in the twitter API object. The tweets are all present in the file `resources/cannedtweets.txt`. These can be edited @@ -43,7 +44,8 @@ def randomtweet(api): alltweets = f.read().splitlines() line = random.choice(alltweets) api.request('statuses/update', {'status':line}) - + + def recentsite(api): """ Tweet one of the recent data uploads from Neotoma. Passing in the twitter API object. This leverages the v1.5 API's XML response for recent uploads. It selects one of the new uploads @@ -82,7 +84,8 @@ def ukrsite(api): tweet = random.choice(records) string = "{name} is a site in Neotoma from the Ukraine πŸ‡ΊπŸ‡¦ https://apps.neotomadb.org/?siteids={id}".format(**tweet) api.request('statuses/update', {'status':string}) - + + def self_identify_hub(api): """ Identify the codebase for the bot through a tweet. """ line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot' From a38cbf377d0e50030cf7838d9293025fc772932c Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Sat, 26 Feb 2022 15:02:36 -0800 Subject: [PATCH 09/13] Updating to make sure things work right the first time. --- neotomabot.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/neotomabot.py b/neotomabot.py index 0526cff..219dbe7 100644 --- a/neotomabot.py +++ b/neotomabot.py @@ -58,6 +58,10 @@ def recentsite(api): records = list(filter(lambda x: x['record']['datasettype'] != 'geochronology' or x['record']['datasetid'] not in datasets, output)) if len(records) > 0: tweet = random.choice(records)['record'] + tweet['geo'] = tweet['geo'].split('|')[0].strip() + while tweet['geo'] == 'Russia': + tweet = random.choice(records)['record'] + tweet['geo'] = tweet['geo'].split('|')[0].strip() while tweet['datasetid'] in datasets: tweet = random.choice(records)['record'] string = "It's a new {datasettype} dataset from the {databasename} at {sitename} ({geo})! https://data.neotomadb.org/{datasetid}".format(**tweet) @@ -91,6 +95,8 @@ def self_identify_hub(api): line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot' api.request('statuses/update', {'status':line}) +ukrsite(api) + schedule.every(6).hours.do(recentsite, api) schedule.every(5).hours.do(randomtweet, api) schedule.every(1).hours.do(ukrsite, api) From ca020acddb630910f0f1d75d5822ae91f6bbb908 Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Sat, 26 Feb 2022 18:15:14 -0800 Subject: [PATCH 10/13] Fixed the URL for sites. --- neotomabot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/neotomabot.py b/neotomabot.py index 219dbe7..7922a08 100644 --- a/neotomabot.py +++ b/neotomabot.py @@ -86,7 +86,7 @@ def ukrsite(api): records = list(map(lambda x: {'id': x['siteid'], 'name': x['sitename']}, list(output)[0]['sites'])) if len(records) > 0: tweet = random.choice(records) - string = "{name} is a site in Neotoma from the Ukraine πŸ‡ΊπŸ‡¦ https://apps.neotomadb.org/?siteids={id}".format(**tweet) + string = "{name} is a site in Neotoma from the Ukraine πŸ‡ΊπŸ‡¦ https://apps.neotomadb.org/explorer?siteids={id}".format(**tweet) api.request('statuses/update', {'status':string}) @@ -95,8 +95,6 @@ def self_identify_hub(api): line = 'This twitter bot for the Neotoma Paleoecological Database is programmed in #python and publicly available through an MIT License on GitHub: https://github.com/NeotomaDB/neotomabot' api.request('statuses/update', {'status':line}) -ukrsite(api) - schedule.every(6).hours.do(recentsite, api) schedule.every(5).hours.do(randomtweet, api) schedule.every(1).hours.do(ukrsite, api) From a960e932fc66d52c59458d49c41fe2a8828f576f Mon Sep 17 00:00:00 2001 From: Simon Goring Date: Sun, 27 Feb 2022 08:33:16 -0800 Subject: [PATCH 11/13] Updated tweet frequency and canned tweets. --- neotomabot.py | 4 ++-- resources/cannedtweets.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/neotomabot.py b/neotomabot.py index 7922a08..11c21d9 100644 --- a/neotomabot.py +++ b/neotomabot.py @@ -86,7 +86,7 @@ def ukrsite(api): records = list(map(lambda x: {'id': x['siteid'], 'name': x['sitename']}, list(output)[0]['sites'])) if len(records) > 0: tweet = random.choice(records) - string = "{name} is a site in Neotoma from the Ukraine πŸ‡ΊπŸ‡¦ https://apps.neotomadb.org/explorer?siteids={id}".format(**tweet) + string = "{name} is a site in Neotoma from Ukraine πŸ‡ΊπŸ‡¦ https://apps.neotomadb.org/explorer?siteids={id}".format(**tweet) api.request('statuses/update', {'status':string}) @@ -97,7 +97,7 @@ def self_identify_hub(api): schedule.every(6).hours.do(recentsite, api) schedule.every(5).hours.do(randomtweet, api) -schedule.every(1).hours.do(ukrsite, api) +schedule.every(3).hours.do(ukrsite, api) schedule.every().monday.at("14:30").do(self_identify_hub, api) while 1: diff --git a/resources/cannedtweets.txt b/resources/cannedtweets.txt index 851f18c..862b6b7 100644 --- a/resources/cannedtweets.txt +++ b/resources/cannedtweets.txt @@ -22,3 +22,4 @@ Learn more about how Neotoma makes the most of teaching and cutting-edge researc Neotoma is on Slack. Come join the discussion and get involved! We're looking for folks to help with documentation, stewardship and coding. https://join.slack.com/t/neotomadb/shared_invite/zt-cvsv53ep-wjGeCTkq7IhP6eUNA9NxYQ Neotoma is at the center of research, engagement and outreach. Find out more in our Elements of Paleontology article: https://doi.org/10.1017/9781108681582 Do you like Diatoms? We're working with @AcadNatSci to get more diatom and water chemistry data into Neotoma. Look how pretty these things are! https://artsandculture.google.com/exhibit/diatoms-of-the-academy-of-natural-sciences-of-drexel-university-academy-of-natural-sciences-of-drexel-university/7QKi7EaVlShRLw?hl=en +The new Neotoma #rstats package is now in beta testing. We're looking for folks to help out with development, testing and documentation. Get involved! https://github.com/NeotomaDB/neotoma2 From 6580f14a884670463ac48e2653635ddab2542b17 Mon Sep 17 00:00:00 2001 From: Erik Zepeda Date: Wed, 26 Mar 2025 12:27:31 -0700 Subject: [PATCH 12/13] Badge-update-README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 750abea..9231825 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,13 @@ # NeotomaBot -[![Lifecycle:Stable](https://img.shields.io/badge/Lifecycle-Stable-97ca00)](https://neotomadb.org) + + +[![Lifecycle:archived](https://img.shields.io/badge/Lifecycle-archived-97ca00)](https://neotomadb.org) [![DOI](https://zenodo.org/badge/417625973.svg)](https://zenodo.org/badge/latestdoi/417625973) [![NSF-1948926](https://img.shields.io/badge/NSF-1948926-blue.svg)](https://nsf.gov/awardsearch/showAward?AWD_ID=1948926) + + A twitter bot to search for new records in the [Neotoma Paleoecology Database](http://neotomadb.org) and then post them to the [@neotomadb](http://twitter.com/neotomadb) Twitter account. This program was an experiment to see how good my Python programming skills are. Apparently they're okay. The code could probably use some cleaning, but I'm generally happy with the way it turned out. From 6f4c07043f46c00ebae61c377699df9fe9ebffef Mon Sep 17 00:00:00 2001 From: Erik Zepeda Date: Thu, 24 Apr 2025 22:33:44 -0700 Subject: [PATCH 13/13] Change Badge Color --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9231825..eeef0c3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ -[![Lifecycle:archived](https://img.shields.io/badge/Lifecycle-archived-97ca00)](https://neotomadb.org) +[![Lifecycle: archived](https://img.shields.io/badge/Lifecycle-archived-orange.svg)](https://neotomadb.org) [![DOI](https://zenodo.org/badge/417625973.svg)](https://zenodo.org/badge/latestdoi/417625973) [![NSF-1948926](https://img.shields.io/badge/NSF-1948926-blue.svg)](https://nsf.gov/awardsearch/showAward?AWD_ID=1948926)